Nothing applies your migrations to production
Most deploy pipelines build your code and ship it. Almost none of them run your database migrations — and the failure that follows is quiet, partial, and easy to misread.
There is a gap in most managed deploy pipelines that nobody tells you about, because each individual piece is behaving correctly.
Your hosting platform builds the application and serves it. Your CI runs the test suite against a test database. Neither of them touches the production database, and neither of them is wrong to leave it alone — applying schema changes to a live database is not something you want happening implicitly, triggered by a merge, with no human watching.
The consequence is that a merge can deploy code which expects tables that production does not have.
What the failure actually looks like
This is the part worth internalising, because the symptom is misleading.
You do not get a site-wide outage. You get one section of the site returning 500 while everything else serves normally. The pages that touch the new tables fail; every page that predates the migration is completely healthy. Your uptime check, which almost certainly requests the homepage, stays green throughout.
If the affected section is linked from your main navigation, visitors find it before you do.
The reason it is quiet is that a partially-migrated database is not a broken database. Connections succeed. Existing queries succeed. Only the queries referencing the new schema fail, and only on the routes that issue them.
The procedure
Treat applying a migration as a deliberate step that follows the deploy, not something you hope happened.
# 1. See what production is actually missing before changing anything.
DATABASE_URL="<production-url>" npx prisma migrate status
# 2. Apply. Always `migrate deploy` — never `migrate dev`, which is
# interactive, can offer to reset, and will try to author new migrations.
DATABASE_URL="<production-url>" npx prisma migrate deployThen hit one page from every route group the migration touches. Not the homepage — the homepage is exactly the page that will lie to you.
Getting the connection string right
Two details cost more time than they should.
If your provider offers both a session pooler and a transaction pooler, use the session pooler. Prisma's driver adapters use prepared statements, and transaction pooling mode does not support them. The failure is not immediate — it appears later, under concurrency, as errors about prepared statements that already exist.
Second, never put the connection string on the command line:
read -rs "DATABASE_URL?production database url: "; export DATABASE_URL
npx prisma migrate deploy
unset DATABASE_URLread -rs does not echo and does not write to your shell history. A production database password in ~/.zsh_history is a credential leak with an indefinite lifetime, and it is a genuinely easy mistake to make while pasting a long command.
The unset matters too. An exported production DATABASE_URL left in a shell will point your next npm run dev at production.
Make it impossible to forget
A procedure that lives in someone's memory is not a procedure. Ours is written into the deployment runbook, and the engineering checklist requires any pull request that adds a migration to say so in its description — so the reviewer sees it, not just the author.
The check that actually catches the gap, though, is the last one: after a deploy that included a migration, load a page from the affected section. It takes ten seconds and it is the only step that tests the thing you care about.