Unbase

Database Migration

Schema-aware, FK-safe row migration into any Postgres.

Unbase reproduces your Supabase Postgres data in a target Postgres without touching the source.

Extract

extract introspects the source and writes the IR:

  • Columns — name, data type, and underlying udt (so arrays, json, and bytea are handled correctly on load).
  • Foreign keys — used to compute a FK-safe load order (referenced tables before referencing ones). Self-references are ignored; true cycles are appended and flagged.
  • Primary keys — used for idempotent loads.
  • Rows — every table is dumped to db/rows/<table>.json, with bigint and bytea kept JSON-safe.

Load

load reads the IR and inserts rows into an existing target schema, in FK-safe order. It is:

  • Idempotent — INSERT ... ON CONFLICT (pk) DO NOTHING, so re-runs and resumes are safe.
  • Id-preserving — original primary keys are inserted verbatim so foreign keys line up.
  • Type-aware — base64 → bytea, JSON objects → json/jsonb, arrays and ISO timestamps pass straight through.

Schema

Unbase can recreate the target schema for you from the snapshot:

npx unbase schema                  # write .unbase/ir/schema.sql
npx unbase schema --apply          # run it against the target
npx unbase schema --orm drizzle    # write a Drizzle schema.ts instead
npx unbase schema --orm prisma     # write a schema.prisma instead
npx unbase load --create-schema    # create, then load, in one step

The emitted DDL is self-sufficient and idempotent: it includes required CREATE EXTENSION (e.g. vector, uuid-ossp), CREATE TYPE for enums, every table with primary keys, and foreign keys added last (so order and cycles don't matter). Or skip this and create the schema with your own Drizzle/Prisma migrations — Unbase loads into whatever already exists.

Not yet emitted: check constraints, non-PK indexes, sequences/identity, generated columns, views, RLS, and triggers. varchar/numeric lengths aren't preserved.

Verify

After loading, Unbase reconciles per-table counts between the snapshot and the target. actual >= expected passes (tolerating pre-existing rows); anything short is reported as a gap.

npx unbase verify

Known limitations

  • GENERATED ALWAYS AS IDENTITY columns are not special-cased yet.
  • Sequences are not resynced after load (resync manually if you rely on serial ids).

On this page