The Prisma migration safety checklist — 12 checks + 6 outage stories
I’ve been on the wrong end of enough 2am Slack messages to पता the pattern. "Hey, the login endpoint is throwing 500s in prod." "The seed script can’t find the users.email column." "Staging is fine, prod is broken, please look." Every time — every time — it traces back to a migration that worked locally, passed CI, और blew up the moment it hit a real database के साथ real concurrent लिखता है और a real up-to-down version gap.
This post is the twelve-check Prisma migration safety list I wish हर migration author kept pinned to their monitor. Each check comes के साथ a short story from an actual outage I या a colleague has shipped. Some names are changed. All of the patterns are real.
At the end I’ll describe the Claude-powered pre-commit hook we’re building that चलती है these checks automatically on हर migration file you stage — लेकिन first, the list.
The 12 checks
Every destructive change has a written-out rollback.
A destructive change is any DROP, any RENAME, any ALTER that changes a column type, any NOT NULL that wasn’t NOT NULL before. Prisma’s default migration engine does नहीं generate a down-migration. You have to लिखना it. If you can’t describe how to reverse the change in SQL before you commit, you don’t have a migration — you have a landmine.
users.email_address to users.email in one migration. Canary was fine. Prod was fine. The next deploy of the application — three हफ़्ते बाद में — rolled back the app image to one that अभी भी expected users.email_address, और the user table was now unreadable to the old code. Fifteen-minute partial outage. Rollback would have been trivial अगर it existed.Column renames are done in two migrations, कभी नहीं one.
A single-migration column rename is almost हमेशा wrong in any environment where the application चलती है across multiple servers या has rolling deploys. The correct pattern: (1) add the new column, backfill it, और have the app लिखना to both; (2) cut the app over to read from the new column; (3) drop the old column. Three deploys. Prisma’s rename shortcut collapses this into one migration — it works सिर्फ़ अगर your app is entirely offline during the migration.
@@map + @map annotation is a frequent false sense of security here. It changes the logical-to-physical mapping लेकिन does नहीं generate the three-step expand-and-contract sequence. A dev assumed it did. 14 मिनट of 500s on the /billing endpoint.Adding a NOT NULL column has a default value या a backfill.
Adding NOT NULL के बिना a default fails on any table के साथ existing rows. Adding it के साथ a default that doesn’t fit your data fails silently — you get हर existing row stamped के साथ the default, और the app now treats those as real values. Three paths are correct: add के साथ a default, add nullable तो backfill तो alter to NOT NULL, या add nullable और leave it that way अगर null is acceptable. Pick one explicitly.
users.plan added as NOT NULL DEFAULT 'free'. Every grandfathered paid user got silently downgraded to free. Nobody noticed for 6 घंटे क्योंकि the billing job ran at midnight. Refund queue for 3 दिन.Foreign keys reference tables that exist at migration time.
Prisma will let you reference a model that doesn’t have its migration applied अभी in some configurations. The migration engine doesn’t हमेशा order migrations by foreign-key dependency when you merge branches. If you’re stacking migrations from multiple feature branches, check that the referenced table’s migration sorts before the referencing table’s migration lexicographically by timestamp.
Organization. Team B added Project के साथ a FK to Organization. Timestamps were within 3 seconds of हर other और the sort order put Project before Organization on deploy. 20 मिनट of ERROR 1452 in prod during the blue-green window.Unique indexes on existing tables don’t conflict के साथ current data.
Adding a @unique on a column where duplicates पहले से exist fails at migration time. Fine locally के साथ an empty dev database. Catastrophic in prod. Before you add a unique constraint, चलती हैं a SELECT column, COUNT(*) FROM table GROUP BY column HAVING COUNT(*) > 1 against a recent prod snapshot.
users.phone_number. Worked on the dev seed. Failed at migration in prod क्योंकि 400 users had a legacy shared "+1-555-HELLO" placeholder. Migration blocked. Deploy blocked. Hotfix at midnight.Indexes created on large tables इस्तेमाल CREATE INDEX CONCURRENTLY.
Prisma does not, by default, generate CREATE INDEX CONCURRENTLY for PostgreSQL. A regular CREATE INDEX holds an ACCESS EXCLUSIVE lock on the table for the duration. On a 50M-row table that’s a मिनट of full lock — meaning हर read और लिखना hangs for that मिनट. Your app will नहीं be dead, लेकिन it will look like it is.
events(tenant_id, created_at) on a 120M-row table took 4 मिनट. The four-minute lock queued enough लिखता है that the connection pool saturated और started timing out other queries. 11-minute partial outage for a 4-minute index creation.The seed script अभी भी works after this migration.
Prisma seeds are separate from migrations लेकिन coupled to the schema. If you drop a column, rename a model, या change a required field, the seed script may अभी भी reference the old shape. This सिर्फ़ bites you when a new developer clones the repo और चलती है prisma migrate reset — अक्सर हफ़्ते after the migration landed.
users.role in favor of a separate UserRole model. Seed script अभी भी did prisma.user.create({ data: { ..., role: 'admin' }}). Two हफ़्ते बाद में a contractor joined, ran reset, got a cryptic Prisma error, filed a ticket, lost half a दिन.Every model’s migration file has a matching TypeScript regeneration.
Merging a migration के बिना running prisma generate pushes a schema change to your co-workers’ branches के बिना the matching type definitions. Their TypeScript compiles fine (because they have the old types); your PR compiles fine; the merged code is broken in a way that slips through both CIs. Every migration PR should include the regenerated node_modules/.prisma/client content check या an explicit step in CI.
Post.slug field was renamed to Post.urlSlug. The migration branch generated fresh types. Three other in-flight PRs merged के साथ stale types. CI green on सभी four. Production build red.Non-null field additions have a narrow time window या a backfill strategy.
On large tables, even adding a nullable column can be slow under heavy लिखना load क्योंकि PostgreSQL rewrites the table. If the column is NOT NULL, you’re also rewriting हर row to set the default. Prefer short pre-announced maintenance windows for these; avoid them during peak traffic.
orders.stripe_customer_id added as NOT NULL DEFAULT '' at 2pm on a Tuesday. Took 90 seconds on the 85M-row table. That’s 90 seconds of elevated p99 on हर checkout. Support load spiked.Migration names describe intent, नहीं बस action.
20260401_add_field.sql बताता है you nothing. 20260401_add_stripe_customer_id_to_orders.sql बताता है the अगला human (or LLM) exactly what to audit. Good migration names are a security control — they surface dangerous changes to reviewers before they have to read SQL.
20260401_update_users.sql क्योंकि the PR description looked fine. Didn’t read the SQL. Migration dropped users.api_key column. API keys revoked across 3,000 tenants. 9-hour recovery.You’ve चलती हैं the migration against a recent prod snapshot locally.
Dev databases are tiny, clean, और schema-current. Prod databases are none of those. Keep a हफ़्ताly-refreshed anonymized snapshot of prod available to हर engineer. Run your migration against it before merging. This catches ~40% of the issues on this list automatically.
Your deployment pipeline halts between prisma migrate deploy और app deploy.
Applying the migration और deploying the new app image should be two separate pipeline steps के साथ a manual (or automated) gate in between. कब something goes wrong, that gate is the difference between "rolled back in 30 seconds" और "schema in state A while app expects state B." Zero-downtime deploys require this gate to be short; observable deploys require it to exist at all.
कैसे the checklist ages
About दो बार a साल I revise this list. Patterns #3 और #4 are ten साल old — they’re नहीं going anywhere. Patterns #6 और #9 are postgres-specific और would be different for MySQL या SQLite. Patterns #8 और #12 are Prisma-era — they didn’t exist on वही list five साल ago, और they’ll शायद evolve as the migration tooling does.
The point of the list isn’t that it’s exhaustive. The point is that the 12 things on it are the 12 things you can’t afford to forget, और सभी 12 of them are detectable from the migration file plus a bit of surrounding context (the seed script, the model definition, the recent schema history). कौन सा means they’re सभी automatable.
The automation layer
Here’s the structural problem के साथ checklists: they require a human to remember to इस्तेमाल them, at the exact moment they’re most tired, on the thing that feels most routine. Friday afternoon. Last commit before vacation. 11pm push after a long दिन.
The right place to चलती हैं this list is नहीं on a PR review 4 घंटे later. It’s at git commit — before the migration has even left your laptop. That’s what we’re building के साथ Septim Guard.
Septim Guard: the pre-commit hook version of this list
Detects any file in your migrations directory (Prisma, Drizzle, TypeORM, Sequelize, Rails, Django, Alembic, Flyway, Liquibase, plus a custom path option). भेजेंs the diff + the surrounding schema context to Claude के साथ a checklist-shaped prompt. Returns a structured verdict in <6 seconds. High-severity findings block the commit. Soft findings warn लेकिन pass. Hard override के साथ --no-verify.
Launch list is $29 founding rate for the पहले 50 seats. Shipping June 2026. Pay $0 now. Reserve your seat →
Until Guard ships: three things you can do this हफ़्ता
Even के बिना a pre-commit hook, you can close ~70% of the gap tonight:
1. Pin this checklist in your repo README
Drop the 12-check list (or a link to this page) in /docs/MIGRATION_CHECKLIST.md और reference it from your PR template under a "Migration safety" checkbox. Make the author tick हर box before the PR is reviewable. Psychological friction beats zero friction.
2. Add a simple prod-snapshot script to your Makefile
make db-snapshot-test: \
pg_dump --no-owner --no-privileges prod | \
psql local-snapshot-db && \
prisma migrate dev --preview-feature
Whatever your version of this is — the idea is to make running your migration against a prod-shaped schema one command, not 15 minutes of VPN.
3. Require two approvers on any migration PR
The single cheapest governance control that actually works. Two humans who have to read the SQL before it merges. Most teams don’t do this because it feels bureaucratic. Most teams also have the outage stories above.
Closing
Migrations fail in the same patterns every time. That’s good news: it means the patterns are reviewable by any reviewer who knows the list — human, checklist, or LLM. The twelve above are the ones that matter. Pin the list. Run the snapshot. Require two approvers. And when Guard ships, run the checklist automatically on every commit, so you don’t have to remember.
Already using Tether?
Septim Tether ($19) runs Claude on your general pre-commit diff. Septim Guard ($29 founding) specializes in migration files with 11 framework presets and schema-aware checks. Tether owners upgrade to Guard for $15 when it launches.
Further reading
- How to set up Claude Code PR review in 2026 (3 options, real tradeoffs) — PR-review pattern survey, covers the CI layer this post doesn’t.
- The Tokenocalypse: why your Claude subagents burned $47K — same hook pattern, applied to cost instead of migrations.
- Septim Guard — the product page with the reservation form.
- Septim Tether — the $19 general-purpose pre-commit hook that Guard is a sibling of.