Unbase

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 + identities

Because 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

PhaseWhat it does
extractIntrospect + dump DB, auth, and storage into the IR (read-only).
storageUpload mirrored objects to the storage target (resumable).
rewriteRepoint Supabase storage URLs in the data to the new target.
loadInsert rows into the target Postgres in FK-safe order.
authMap users/identities into Better Auth (bcrypt preserved).
verifyReconcile 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.

On this page