Moving from Supabase
Keep the client. Change what operates it.
Backenly's data plane is PostgREST, so a supabase-js frontend keeps its query grammar. What you gain is the governed change path and the autonomy loop.
- Who this is for
- Teams on Supabase who want the operator, not a new client
- What you already have
- A supabase-js frontend and a Postgres schema
- What you need
- Governance without a frontend rewrite
The problem
Supabase is good infrastructure, which is exactly the issue when nobody on the team wants to own schema design, migrations, and RLS review. The assembly and the upkeep are yours. Switching platforms normally means rewriting every call site, which is why teams stay past the point where it is working for them.
What you would normally build
A rewrite of every data call, a new auth integration, a re-derivation of every security policy in a different dialect, and a migration window long enough to do all three.
What Backenly does
Swap
Change the import and the client construction
The compatibility entry point is built on the v2 surface, which passes PostgREST's grammar through untouched. It emits PostgREST rather than translating into a narrower dialect, which is why it is complete rather than approximate.
Keep
Filters, embeds, upsert, and counts behave
or(), select('*, author(*)') embeds, overlaps(), upsert with onConflict, and count: 'exact' via Content-Range. Every operation resolves { data, error } and never throws, which is the convention the frontend is written against.
Move
Bring the data and re-author the policies
pg_dump in, then take a connection string and run your migrations, then reconcile with adopt_external_schema. Policies are re-authored in Backenly's claim form, where set_rls installs your predicate verbatim and reads pg_policies back before reporting success.
Gain
The part that was not on offer before
A governed mutation kernel, behavioural verification after changes, and a loop that monitors and repairs without being asked.
// before
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('https://xyz.supabase.co', SUPABASE_ANON_KEY)
// after
import { createClient } from '@backenly/sdk/supabase'
const supabase = createClient(
'https://backenly.com/api/v1/<PROJECT_ID>',
BACKENLY_ANON_KEY,
)
// unchanged — this is PostgREST on both sides
const { data, error } = await supabase
.from('posts')
.select('*, author(*)')
.or('published.eq.true,pinned.eq.true')
.order('createdAt', { ascending: false })What you end up with
The same query grammar, the same { data, error } contract, and the same realtime subscription shape — over a backend where structural change is governed, verified, and reversible, and where a loop is watching between your deploys.
Who owns what
Backenly does
- Serves PostgREST's grammar verbatim on the v2 surface, embeds included.
- Maps auth and postgres_changes subscriptions onto Backenly equivalents.
- Installs your RLS predicates verbatim and reads them back before reporting success.
- Refuses what it cannot honour rather than approximating it.
You own
- The data migration and the cutover window.
- Re-authoring policies into the claim form, and testing them.
- Replacing any rpc() call sites — there is no SQL-function surface.
- Deciding whether the governance trade is worth it for your team.
What this is built on
| Capability | What it does here |
|---|---|
| @backenly/sdk/supabase | Compatibility entry point emitting PostgREST directly, with { data, error } semantics and no throws. |
| /api/v2/{projectId}/{table} | PostgREST grammar passed through untouched — filters, ordering, embeds, Prefer headers, Content-Range counts. |
| set_rls | Policies as exact SQL, installed verbatim, read back from pg_policies. No model in the path. |
| pg_dump | Full schema export in either direction, so the move is reversible. |
| Autonomy loop | Continuous monitoring and repair of the reversible safe band — the capability that has no Supabase equivalent. |
Known limitations
- rpc() is refused. Backenly exposes no SQL functions by design, so any stored-procedure call sites need re-homing as event, cron, or HTTP functions.
- Policies must be re-authored. They are not translated automatically from Supabase's dialect.
- Storage and auth are Backenly's implementations, not Supabase's. The compat layer maps the common auth calls; anything provider-specific needs checking.
- This is a client-compatibility bridge, not a one-click migration. Moving data and cutting over remain manual work.
Common questions
Does my existing query code really work unchanged?
The common path does, because both sides are PostgREST clients and the mapping is largely the identity function. Filters, column projection, embedded resources, upsert with onConflict, and exact counts pass through. Test rpc() call sites and anything provider-specific before cutting over.
What about realtime subscriptions?
channel().on('postgres_changes', …) maps onto Backenly realtime, which is PostgreSQL LISTEN/NOTIFY over Server-Sent Events through a shared listener hub. No WebSocket server and no Redis on this side.
Why would I move at all?
Only one reason is worth the work: you want the operating layer. Supabase hands you excellent primitives and you own the assembly and upkeep. Backenly governs every structural change, verifies it behaviourally, and runs a repair loop between your deploys. If your team enjoys owning that, Supabase is a good product and you should stay on it.
Try it on one free project
No credit card. Connect your agent over MCP and judge it by the verification evidence.