This runbook describes how to safely roll back database migrations for this project.
- Migration tool:
drizzle-kit(paired withdrizzle-orm). - Migration files: stored in
drizzle/(SQL files) with metadata indrizzle/meta/. - Database: Supabase (PostgreSQL).
- Applied migrations tracking: Drizzle records applied migrations in the
__drizzle_migrationstable in thedrizzleschema.
⚠️ Drizzle does not generate "down" migrations automatically. Rollbacks are performed either by applying a hand-written reverse SQL migration or by restoring from a Supabase backup. Choose the approach that matches the change.
Inspect the migrations Drizzle has recorded as applied:
# Using psql against the Supabase connection string
psql "$DATABASE_URL" -c "SELECT id, hash, created_at FROM drizzle.__drizzle_migrations ORDER BY created_at;"You can also list the migration files present locally:
ls -1 drizzle/*.sqlCompare the two lists to determine which migrations are applied vs. pending.
Drizzle has no built-in down command, so roll back by applying a reverse migration.
Scripted (recommended): write the reverse SQL (step 2 below), then let
scripts/db-rollback.sh apply it and remove the
latest row from drizzle.__drizzle_migrations in a single transaction:
# Rehearse: runs everything, then rolls the transaction back
DATABASE_URL=... ./scripts/db-rollback.sh --file drizzle/9999_rollback_last.sql --dry-run
# Apply (prompts for confirmation when --env production, unless --yes)
DATABASE_URL=... ./scripts/db-rollback.sh --file drizzle/9999_rollback_last.sql --env productionIf the reverse SQL fails, nothing is committed. The manual equivalent is:
-
Identify the last applied migration:
psql "$DATABASE_URL" -c "SELECT id, hash, created_at FROM drizzle.__drizzle_migrations ORDER BY created_at DESC LIMIT 1;"
-
Write a reverse SQL migration that undoes the change (e.g.
DROP TABLE,DROP COLUMN, restore prior constraint). Save it as a new file, e.g.drizzle/9999_rollback_last.sql. -
Apply the reverse migration:
psql "$DATABASE_URL" -f drizzle/9999_rollback_last.sql -
Remove the corresponding row from the migrations table so Drizzle no longer considers the original migration applied:
psql "$DATABASE_URL" -c "DELETE FROM drizzle.__drizzle_migrations WHERE id = '<migration_id>';"
-
Verify with the checklist in section 5.
Prefer creating a new forward migration that reverses the change rather than deleting history, when possible. Deleting rows from
__drizzle_migrationsis only appropriate when you are certain no other environment depends on that migration record.
To roll back more than one migration in a single run, pass --steps with a
reverse SQL file that undoes each of those migrations, newest first.
scripts/db-rollback.sh applies that file and
deletes the same number of newest rows from drizzle.__drizzle_migrations
in one transaction:
# Rehearse reverting the three newest migrations
DATABASE_URL=... ./scripts/db-rollback.sh \
--file drizzle/rollback_to_<target>.sql \
--steps 3 \
--dry-run
# Apply (prompts for confirmation when --env production, unless --yes)
DATABASE_URL=... ./scripts/db-rollback.sh \
--file drizzle/rollback_to_<target>.sql \
--steps 3 \
--env production--steps defaults to 1. The script refuses to continue when fewer than
<n> migrations are recorded.
The manual equivalent is:
-
List applied migrations in reverse chronological order:
psql "$DATABASE_URL" -c "SELECT id, hash, created_at FROM drizzle.__drizzle_migrations ORDER BY created_at DESC;"
-
Determine the target migration you want to keep. Everything applied after it must be reversed.
-
Write reverse SQL for each migration to be undone, in reverse order (newest first). Combine them into a single script, e.g.
drizzle/rollback_to_<target>.sql. -
Apply the reverse script:
psql "$DATABASE_URL" -f drizzle/rollback_to_<target>.sql
-
Delete the corresponding rows from the migrations table:
psql "$DATABASE_URL" -c "DELETE FROM drizzle.__drizzle_migrations WHERE id IN ('<id1>', '<id2>', ...);"
-
Verify with the checklist in section 5.
Use this when a rollback cannot be expressed as reverse SQL, or when data corruption occurred.
-
Open the Supabase Dashboard → your project → Database → Backups.
-
Choose a point in time prior to the problematic migration. Supabase retains PITR backups based on your plan's retention window.
-
Initiate the restore. Supabase will provision a restored instance. Note the connection details of the restored database.
-
Validate the restored data before switching traffic:
- Confirm the target migration state in
drizzle.__drizzle_migrations. - Spot-check critical tables and row counts.
- Confirm the target migration state in
-
Repoint the application to the restored database by updating
DATABASE_URL(and any Supabase keys if the project reference changed). -
Re-apply any migrations that were intentionally made after the restore point, using
drizzle-kit:npx drizzle-kit migrate
-
Verify with the checklist in section 5.
⚠️ PITR restores are destructive to the current database state. Always take a manual snapshot or confirm the restore target before proceeding.
- Confirmed the migration(s) to be rolled back and their IDs.
- Captured a fresh backup / snapshot of the current database.
- Notified stakeholders and confirmed a maintenance window if needed.
- Application traffic paused or put into read-only mode (if applicable).
- Reverse SQL script reviewed by a second engineer.
- Verified
DATABASE_URLpoints to the intended environment (not production by accident).
- Reverse SQL applied without errors.
-
drizzle.__drizzle_migrationsreflects the expected state:psql "$DATABASE_URL" -c "SELECT id, created_at FROM drizzle.__drizzle_migrations ORDER BY created_at;" - Schema matches expectations (tables, columns, constraints, indexes).
- Application starts successfully and connects to the database.
- Smoke tests pass (auth, core read/write paths).
- No orphaned or missing foreign key references.
- Logs show no migration- or schema-related errors.
- Stakeholders notified that rollback is complete.
- Always run migrations and rollbacks against a staging environment first.
- Keep reverse SQL scripts in version control alongside the forward migrations.
- If unsure, prefer restoring from a Supabase backup over ad-hoc SQL edits.