Last Updated: 2026-04-24
Related:
BRANCHING_CI_SPIKE.md (CLI/API options for Branching + CI)
This repo enforces two lanes for new SQL under supabase/migrations/ so production receives DDL + idempotent system catalogs, not ad-hoc tenant/test data.
Lanes
1. schema (default)
- Use for: tables, indexes, constraints, RLS policies, functions, triggers, views, extensions, cron wiring that uses vault /
current_setting— no tenant data. - Rules: After migration timestamp cutoff (see below), no top-level
INSERT/UPDATE/DELETE. Statements inside$$ … $$function bodies are ignored for this rule. - Marking: No header required (default lane).
- Partition safety: For active high-volume tables, phase-1 partition migrations (shadow table + sync trigger + helper functions) belong in
schemalane; phase-2 cutover must be promoted separately with runbook evidence (ADR-020).
2. system-defaults
- Use for: global catalogs the product expects in every environment (
pf_module_permissions, jurisdiction profiles, notification templates, etc.) — always idempotent. - Rules:
- First line of file:
-- @lane: system-defaultsor filename containssystem_defaults/system-defaults. - Every
INSERTmust includeON CONFLICT …and/orWHERE NOT EXISTS/NOT EXISTSin the same statement (rough static check in CI). UPDATE/DELETEfor catalog corrections are allowed in this lane (e.g. renaming seeded display labels).
- First line of file:
Cutoff (strict vs patterns-only)
CI runsscripts/database/check-migration-lane.ts on changed migration files only.
- All scanned files: hard fails on a hardcoded Supabase project host — any of the retired (
anuwknikgsijbameytzr.supabase.co,aximlomrwfuhctxjhrsf.supabase.co) or active (anuwknikgsijbameytzr.supabase.codev2,aximlomrwfuhctxjhrsf.supabase.coprod2) refs — plus JWT-like literals (eyJ…) and known dev-only bootstrap UUIDs. Migrations must read connection details fromvault.decrypted_secrets/env, never embed a project host. - Strict DML rules (schema vs system-defaults) apply only when the migration filename’s timestamp prefix is ≥
MIGRATION_LANE_STRICT_AFTER(default20260424160000). Older files in the same PR are still checked for banned patterns only.
What does not belong in migrations
- Staging/demo organizations, users, employees, residents — use
supabase/seeds/and SQL Editor / seed runners for non-prod only. See supabase/seeds/README.md. - Secrets in SQL (anon JWT, service keys). Use
vault.decrypted_secretsand Edge Function secrets.
Migrations-only-forward (direct-apply policy)
The migration files insupabase/migrations/ are the single source of truth for schema and system-default state on dev2 (anuwknikgsijbameytzr) and prod2 (aximlomrwfuhctxjhrsf).
- Forward changes go through a migration file. New schema or system-default changes are added as a new timestamped migration; do not edit applied migrations.
- Any direct remote apply (Supabase MCP
execute_sql, the SQL Editor, orpsqlagainst a live project) MUST be accompanied by an equivalent local migration file in the same change/PR, so the repo ledger and the database never diverge. supabase-drift-check.ymlis the backstop — it links the project via theSUPABASE_{DEV,PROD}_PROJECT_IDsecrets (which must point at dev2/prod2) and fails if the live schema drifts from the committed migrations. A red drift check means a direct apply was made without a matching migration.
Seed-style rows with foreign keys
Migrations that insert test fixtures referencing rows that exist only after full seed runs (e.g.pf_profiles as pf_documents.author) must guard inserts—typically IF EXISTS (SELECT 1 FROM …) inside a DO $$ … $$ block—so supabase db diff --linked shadow databases and CI drift gates do not fail with FK violations. Example: 20260313162459_5b58857b-e8d5-4ef0-bd40-ec6a2d305736.sql (PF-11 Phase 5 verification seed).
Commands
npm run db:check:migration-lane -- --since=<sha>
Note: Invoking check-migration-lane.ts with no --since, --ref-range, or --full-repo-audit prints a skip message and exits 0 (no files scanned). Custom jobs must pass one of those flags.
Legacy inventory (retrofit lane headers)
One-off or periodic reports for classifying historical migrations (DDL-only vs system-defaults vs ambiguous):reports/db/migration-lane-inventory.json and reports/db/MIGRATION_LANE_INVENTORY.md. To apply high-confidence -- @lane: system-defaults first-line headers without changing SQL semantics:
Release PR summary (git range + Dashboard links)
Used by.github/workflows/supabase-promote-pr.yml (see accuracy note below — not currently present): lists changed migration paths vs the PR base SHA, links to Supabase Dashboard for the prod project (aximlomrwfuhctxjhrsf), and optionally appends a redacted unified diff (eyJ…-style tokens → [REDACTED_JWT]).
Phased strictness (cutoff)
- Today: strict lane rules apply to migrations with filename timestamp ≥
MIGRATION_LANE_STRICT_AFTER(default20260424160000); older files get banned-pattern checks when touched, or full-repo patterns-only under the audit workflow below. - Phase B:
.github/workflows/migration-lane-audit.yml(see accuracy note below — not currently present) runs--full-repo-auditon a schedule (andworkflow_dispatch), uploads a Markdown artifact, and does not block merges: the scan step usescontinue-on-error: true, so the workflow can stay green while the artifact still lists violations (informational triage). - Later: lower
MIGRATION_LANE_STRICT_AFTERin steps only after inventory shows the target slice is clean; optional end state is strict on all files once headers and catalog DML are aligned.
CI wiring
Accuracy note (2026-05-26): This table now lists only workflows that actually exist in.github/workflows/. Several previously-referenced workflows (supabase-promote-pr.yml,db-migration-guard.yml,migration-lane-audit.yml,supabase-bootstrap-prod.yml) were documented but never implemented and have been removed from this table.supabase/config.toml’s comment block still references some of these; it is Lovable-auto-generated (do-not-hand-edit), so correct it at the generator if it regenerates.
Author-time allocator (§5.8, 2026-06-09): mint a collision-resistant version withnpm run -s db:migration:next-version(append a slug:npm run -s db:migration:next-version -- fa_add_thresholds→<version>_fa_add_thresholds.sql). It returns the next free opaque 14-digit version above the max of local +origin/developmentmigrations (notdate +%sraw), so concurrent worktrees are less likely to mint the same prefix. For the CLI-generated paths (supabase db diff -f/supabase migration new), which stamp their own wall-clock prefix, runnpm run db:migration:reversionright after generating to pull the file aboveorigin/development;.husky/pre-pushrunsdb:migration:reversion:checkso a below-base migration is caught before the PR. This is ergonomics only — the deploy-boundary guard below is the actual collision guarantee. When a collision does occur, follow the one blessed path inMIGRATION_COLLISION_RESOLUTION.md(npm run db:migration-versions:fix) rather than re-researching a bespoke fix. Concurrency caveat (2026-06-09): the migration version-collision guard insupabase-deploy-dev.yml/supabase-deploy-prod.ymlreads the remote ledger then gatesdb push.supabase-realign.ymlwrites the same ledger (db push
migration repair) under a separate concurrency group. Do not runsupabase-realignconcurrently with a deploy — the guard assumes no out-of-band ledger writes during a deploy. Re-baseline/realign during a quiet window only.
Current baseline (2026-05-28)
The active migration set is a single validated 2-file baseline (the prior 1,150 files are archived undersupabase/migrations/_archive/):
00000000000000_baseline_2026_05_28_ddl.sql— full schema (1225 tables, 4459 policies, RLS 100%).00000000000001_baseline_2026_05_28_data.sql— global system-default catalogs (loaded withsession_replication_role = replica).
YYYYMMDDHHMMSS_* timestamps (they sort
after the 0000… baseline). Keep remote schema_migrations in agreement with the
repo using npm run db:reconcile:<env> (see
2026-05-28-new-projects-operator-runbook.md).
Provenance/parity: _archive/README.md.
Re-baseline cadence
The migration set reached 1,150 files before the 2026-05-28 squash. To stop it exploding again:- Trigger: when the active (non-archived) migration count exceeds ~150.
- Procedure: run Phase 1 of
the realignment plan —
replay the repo into a throwaway DB, diff vs dev, dump a cleaned baseline,
archive the old files under
supabase/migrations/_archive/, thensupabase migration repairdev + prod to the new baseline. - Churn policy: the pre-commit filename lint
(
scripts/database/lint-migration-filename.ts) blocks new UUID-suffix names for human commits, but Lovable’s bot bypasses the local pre-commit hook, so UUID-named migrations still arrive. Rather than fight this, tolerate them between baselines and fold them into the next baseline. The enforced gate is apply-cleanliness (supabase-ci.yml) + drift visibility (supabase-drift-check.yml), not naming.