Database Migration Policy
Last updated: 2026-04-21 · Ticket: NQU-652 (staging-to-prod ADR audit)
Policy
Every new migration file must be paired with a rollback file in the same directory. The existing 86 pre-policy migrations are not being backfilled — retrofitting rollbacks for every historical migration is impractical and higher-risk than the policy is worth.
Going forward, PRs that add a new file under supabase/migrations/ without a matching .down.sql should be sent back for revision.
File format
One migration = a pair of files under supabase/migrations/:
<timestamp>_<short-name>.sql # forward migration — runner applies this
<timestamp>_<short-name>.down.sql # rollback — humans apply manually
Use the templates as starting points:
supabase/migrations/TEMPLATE.up.sql.examplesupabase/migrations/TEMPLATE.down.sql.example
The runner (scripts/run-migration.ts) glob-filters out *.down.sql from the forward run. Both files remain in the repo so the paired rollback is discoverable next to its forward migration.
When rollback is genuinely impossible
Some migrations cannot be cleanly reversed — e.g. dropping a column with data, merging tables, or lossy transformations. In that case:
- State it explicitly in a comment inside the
@DOWNsection. - Document the data-loss implication (what gets lost, who would notice).
- Add a manual pre-apply snapshot step to the PR description: "Before merging this to main, take an RDS snapshot named
pre-NQU-XXXvia AWS console."
Before running a migration against production
- Take a manual RDS snapshot (
invapp-dev-rds-<timestamp>-pre-<ticket>). The nightly automated snapshots aren't frequent enough for surgical rollback of a just-applied migration. - Review the migration one more time against production state. Migrations that passed CI can still fail in prod if rows violate new constraints, indexes conflict, etc.
- Run during a low-traffic window if the migration holds locks.
Testing the DOWN section
Before landing a migration, verify the rollback by:
- Running the UP section against a local or staging DB.
- Running the DOWN section immediately after.
- Confirming the schema looks like it did before (
pg_dump --schema-onlydiff, for example).
If the DOWN section throws errors or leaves residue, fix it before merging.
CI / runner behavior
npm run db:migrateexecutes only the UP section of each pending migration.- The
_migrationstracking table stores the filename. Re-running a migration after rollback requires manually clearing that row. - CI runs migrations against a fresh Postgres container, so UP-only verification catches most issues — but does NOT verify that DOWN works. Test DOWN locally.
Related
- NQU-652 (staging-to-prod ADR) — parent ticket
docs/decisions/2026-04-03-staging-to-production-transition.md— original ADRdocs/admin/ops/deployment-flow.md— end-to-end deploy pipeline