Migrate from Supabase
Move the Postgres database out of a Supabase project into CapyDB.
Scan first: which Supabase exit are you making?
capydb migrate scan # read-only; no login neededSupabase migrations differ wildly by how much of the platform you use, and the scan grades yours
by counting call sites per kind - data (supabase.from()), auth, storage, realtime:
- Auth on Clerk (or similar), data via a direct Postgres driver - the DB move is an env swap plus storage/realtime replacement. Small.
- Auth on Clerk, data via
supabase.from()- rewrite the data layer to a Postgres client first, against your current Supabase DB (it's plain Postgres), then import. The rewrite size is the call-site count the scan prints. - Supabase Auth is your identity system - a DB-only move buys nothing and breaks login. Migrate auth first; see Auth systems and your migration.
- BetterAuth / Auth.js / Lucia - their tables live in your schema and ride the dump. Nothing auth-related to do.
The preflight (below) reports the database-side version of the same facts: foreign keys into
auth.users, tables with auth.uid() RLS policies, and schemas beyond public.
Get the source connection string
In the Supabase dashboard: Project → Connect (top bar). You will see three flavors - pick carefully:
- Direct connection (
db.<ref>.supabase.co:5432) - best for imports, but it is IPv6-only on most projects unless the IPv4 add-on is enabled. - Session pooler (
aws-0-<region>.pooler.supabase.com:5432) - IPv4-reachable and fine for an import (it behaves like a session-mode connection). - Transaction pooler (port
6543) - rejected outright by preflight and import (a logical copy cannot run through transaction pooling).
Practical rule: use the session pooler URL unless you know your project has IPv4 enabled on the direct host. The username on pooler URLs includes the project ref (postgres.<ref>), which is expected.
Supabase-specific caveats
- You are migrating the database, not the platform. Supabase Auth, Storage, Edge Functions, and Realtime do not come along. When the importer sees a Supabase source URL it automatically excludes the platform-managed schemas (
auth,storage,realtime,extensions,graphql,pgsodium,vault,supabase_functions, and friends) - only your application schemas (publicand any you created) are copied. If you use Clerk, CapyDB's Clerk auth sync covers the user-table-in-your-database pattern. - RLS policies are dropped during the import. They reference
auth.uid()and theanon/authenticatedroles, none of which exist in plain Postgres - a single one would abort the whole restore. Your app connects as the database owner (which bypasses RLS), so this changes nothing at runtime; if RLS was doing real authorization work via PostgREST, that logic must move into your application layer before cutover. - Extension schema layout is mirrored. Supabase installs extensions into an
extensionsschema, so your column defaults look likeextensions.uuid_generate_v4(). The importer recreates that layout on the CapyDB side before restoring, so those defaults keep resolving. - Extensions: the CapyDB allowlist covers the common Supabase set -
pgcrypto,uuid-ossp,pg_trgm,citext,hstore,pg_stat_statements,vector(pgvector),postgis,unaccent,ltree,fuzzystrmatch,btree_gin,btree_gist. Supabase-only machinery (pg_graphql,pgsodium,supabase_vault) lives in the excluded schemas and never reaches the preflight. - Foreign keys into
auth.usersare the one thing that can still fail the restore: the excludedauthschema means such a constraint has no target. Drop those FKs on the source (or re-point them at your own users table) before importing.
Preflight
capydb import preflight --source-url "postgres://postgres.<ref>:password@aws-0-eu-central-1.pooler.supabase.com:5432/postgres"Expect the extension check to be the interesting one. Fix failures before the window, not during.
Import
Pause writers, then:
capydb import --source-url "postgres://..." --recreate --waitAfter the import
- Verify row counts with
SELECT count(*)per table, and spot-check sequences on hot tables. \dx- verify the extension set; drop leftover Supabase-specific schemas you decided not to keep.ANALYZE;- Cut over
DATABASE_URL(pooled:6432for app traffic, direct:5432for migrations) on every deployable that uses this database, and deploy them together - a half-swapped pair of services silently splits writes across two databases. - Confirm the exit before you pause the Supabase project:
capydb migrate verify --source-url "postgres://postgres.<ref>:...@aws-0-<region>.pooler.supabase.com:5432/postgres"Supabase's own platform connections (PostgREST, exporters, admin roles) are filtered out; anything listed is one of your deployables still pointed at the old database. Keep the Supabase project paused - not deleted - for a rollback window.