The answer. The CLI (npm i -D supabase, then npx supabase …, pinned per project so teammates and CI agree on a version) runs the entire platform in Docker. supabase init writes supabase/config.toml, which you commit; supabase start boots Postgres, Auth, PostgREST, Storage, Realtime, Studio and a mail catcher, and prints the local keys. Two files carry your state between machines: migration files under supabase/migrations/ (structure, generated from a running database with db diff -f name or hand-written with migration new) and supabase/seed.sql (data, which runs only on a fresh database or db reset). Types come from the schema (gen types typescript), so regenerate them in the same commit as the migration.
The pattern.
npx supabase start # API 54321 · Postgres 54322 · Studio 54323 · mail catcher 54324
npx supabase migration new add_tickets # hand-written SQL, or:
npx supabase db diff -f add_tickets # capture what you changed in Studio
npx supabase db reset # replay migrations + seed.sql from scratch
npx supabase gen types typescript --local > packages/db/types.ts
npx supabase link --project-ref <ref> && npx supabase db push --dry-run # what would apply remotely
Watch out.
db pushapplies migrations by comparing file names against the remote's list, not by diffing schemas. A file edited after it was applied is silently skipped; a table created by hand on the server is never noticed.db resetalso turns Realtime off on every table and drops UI-only settings; capture those in migrations too.- Extensions installed in
pg_catalog(pg_cron) are missed bydb diff; write thatcreate extensionline yourself.
Related: tracked-migration-ledger-checksum-same-transaction · branching-gives-each-pr-a-database-without-your-data · first-table-enable-rls-write-one-policy-query-it