How It Works
The snapshot (IR), adapters, and the six phases.
The snapshot (IR)
Every migration flows through a portable, on-disk intermediate representation
(IR) — a directory (default ./.unbase/ir) that fully describes the source:
.unbase/ir/
manifest.json summary + counts
db/schema.json tables, columns, foreign keys, FK-safe load order
db/rows/<table>.json extracted rows (JSON-safe)
storage/snapshot.json object listing + reconciliation
storage/objects/... mirrored bytes (with --mirror)
auth/snapshot.json normalized users + identitiesBecause extraction writes the IR and loading reads it, the two are decoupled: any source can target any destination, and a migration is resumable at any phase — re-running is safe.
Adapters
Each source and target is an adapter behind a stable interface:
- Sources:
supabase-postgres,supabase-storage,supabase-auth - Targets:
postgres,s3(R2/S3/GCS/MinIO),better-auth
Supporting a new database, bucket, or auth system means writing one adapter — nothing else changes.
The six phases
| Phase | What it does |
|---|---|
extract | Introspect + dump DB, auth, and storage into the IR (read-only). |
storage | Upload mirrored objects to the storage target (resumable). |
rewrite | Repoint Supabase storage URLs in the data to the new target. |
load | Insert rows into the target Postgres in FK-safe order. |
auth | Map users/identities into Better Auth (bcrypt preserved). |
verify | Reconcile target row counts against the snapshot. |
Safety
- Source is never mutated — every source adapter is read-only.
- Target changes are additive — loads use
ON CONFLICT DO NOTHING, so they are idempotent and safe to re-run. - Original ids are preserved, so foreign keys line up across the migration.