Production is migrated by hand, by Matt (ADR 0026). apps/remote deploys on every merge, and its build (apps/remote/scripts/assert-migrations-applied.mjs) fails on purpose when production lacks the commit's latest migration, so the previous deployment keeps serving. You merge, Matt migrates, then you redeploy.
You never run pnpm db:migrate against production, and you never write to production by any other route either. packages/core/drizzle-guard.ts refuses most wrong runs, but don't rely on it.
1. Generate and merge
- Change
packages/core/db/schema.ts, then runpnpm db:generate. Commit the generated SQL and themeta/files unedited. - The migration and the code that uses it may ship in one PR. It must be additive: a new table, a nullable column, or a column with a default. For anything destructive, see section 4.
- Land it the usual way (
docs/agents/merging.md).
2. The deploy fails, and Matt migrates
The apps/remote deploy for the merge commit fails with production has not applied <tag>. That failure is expected and harmless: the old deployment is still serving.
Send Matt exactly one message with the migration tag and this command, run from his main checkout:
git switch main && git pull --ff-only && pnpm db:migrate
Then wait. Do not redeploy, retry or poll until Matt confirms he has run it.
3. Redeploy and confirm
-
Find the failed deployment for the merge commit:
npx vercel@latest ls course-video-manager-remote --scope matt-pococks-projects -m githubCommitSha=<merge-sha> --jsonTake the
urlof theproductiondeployment whosestateisERROR. -
Rebuild it. The CLI is already logged in on Matt's machine. The command waits for the build and prints
Readyon success.npx vercel@latest redeploy <url> --scope matt-pococks-projectsIf the build fails again with
production has not applied, Matt's migrate did not take. Tell him, quoting the tag. Do not redeploy in a loop.redeployof the merge commit's deployment is the only way you put anything on production. Never runvercel deploy --prod(orvercel --prod): it uploads your working tree, notmain. The build refuses it anyway (apps/remote/scripts/assert-production-source.mjsfails any production build that is not a GitHub clone ofmain), but don't rely on that. Ifredeployfails withassert-production-source, you redeployed the wrong deployment: stop and tell Matt. -
Confirm on GitHub. Read the commit's status contexts and check that
VercelisSUCCESSwith acreatedAtafter your redeploy:gh api graphql -f query='{repository(owner:"mattpocock",name:"course-video-manager"){object(expression:"<merge-sha>"){... on Commit{status{contexts{context state description targetUrl createdAt}}}}}}'Use GraphQL. The REST
commits/<sha>/statusesendpoint has returned HTTP 500 for this repo.Unproven. The
vercel redeploycommand has been proven to rebuild and promote production. Whether that redeploy posts a freshVercelstatus to the commit has not been confirmed yet. Until Matt confirms it once, also report theReadyline and the URL thatvercel redeployprinted.
4. Destructive changes go contract-first
Dropping or renaming a column or table, or making a column NOT NULL without a default, would break the code already serving. Run it in two PRs:
- Stop using it. Remove the column from
schema.tsand every read and write of it. Merge, and letapps/remotedeploy green. - Drop it. Only then generate and merge the migration that drops it, and follow sections 2 and 3.
A rename is add-new → copy the data → switch the code → drop-old, with each step shipped as above.
Local development
On Matt's machine, apps/local uses the production database too. pnpm dev and pnpm start print a loud warning when that database is behind the checkout's migrations (apps/local/scripts/warn-pending-migrations.ts). The fix is the same command from section 2, run by Matt.