Skip to content

Database Migrations

Migrations are the only way the schema changes. Live DB edits (Studio) are not a source of truth.

Rules [ENFORCED]

  • Filename: 14-digit YYYYMMDDHHMMSS_name.sql. Prefer npx supabase migration new {name}. Never the old NNN_ form — it collides on the version parser.
  • Never edit a merged migration — add a new one.
  • Expand-contract, always. Never a breaking change in one step.
  • RLS on every user-facing table. Append-only tables carry no UPDATE/DELETE policy.
  • Use row_to_json(table.*) in RPCs for forward-compat.
  • Capture pg_cron schedules and storage buckets as migrations, not just live state.

Expand-contract

Each arrow is (potentially) a separate migration + deploy. The app is always runnable at every step — old and new code both work against the intermediate schema.

The baseline

The core product schema is captured once as a schema-only squash: supabase/migrations/20260710134136_baseline_schema_from_prod.sql (supersedes 3 archived pre-baseline files — see _archive_pre_baseline/README.md). Verified 58/58 tables and 118/118 RLS policies identical between Dev and Prod, zero data rows on Dev.

Workflow

bash
npx supabase migration new add_thing_flag      # creates the timestamped file
# ...write expand-contract SQL + RLS...
# apply to Dev, verify, then Prod (see Promotion)

Promotion (mandatory)

Every migration is promoted to both Supabase projects deliberately (Dev first, then Prod). Dev/UAT does not self-sync to Prod. Full procedure + parity-verification SQL in supabase/docs/PROMOTION_RUNBOOK.md. See Deployment & Promotion.

[TRANSITIONAL] — not yet on Dev

  • pg_cron job — hardcodes Prod's Edge Function URL; needs a project-URL-aware rewrite first.
  • profile-pictures storage bucket — environment-specific data, not portable schema.

Rollback (DR)

Backend rollback = a compensating expand-contract migration (not an in-place revert). Supabase managed daily backups are the interim safety net; PITR + formal RPO/RTO are a deferred decision.