There are people in your database now. Some of them are mid-task while you are reading this, and the next thing you deploy lands underneath them without warning.
Nothing about how you write code needs to change. What changes is the size of the step you take and whether you know, before you take it, how to get back.
The idea
Every deploy is a bet that the new version is at least as good as the old one. You lose that bet regularly, so the question worth asking before you ship is how fast you can undo this, and whether undoing it actually restores the previous state. For application code the answer is almost always yes and in seconds. For anything that changed the shape or contents of your database, the answer is often no.
git main production
-------- ----------
commit A ---- deploy ----> [A] running fine
| |
commit B ---- deploy ----> [B] 500 errors on /checkout
|
rollback: redeploy A (code: ~60 seconds)
|
but B's migration dropped orders.coupon_code
and A still reads that column
|
v
A is broken too. restore from backup.How it works
Small changes, one at a time. When you ship one change and something breaks, you know what broke it. When you ship nine, you have a bisection problem on a live site with users watching. This is the whole reason to resist the session where the AI also cleaned up four unrelated files.
A rollback is a plan you have run. On Vercel, Railway or Netlify, previous deploys stay available and you promote an old one from the dashboard. On your own server it means redeploying the previous commit, or pointing back at the previous build if you kept one. Do it once deliberately while everything is fine, so you know the button, the command, and how long it takes.
Not everything has to be tested on the people using it. Vercel gives every push to a non-production branch its own preview URL automatically. Netlify does that for pull requests, and for plain branches once you add them under branch deploys. On Railway you switch on PR environments first. All of them give you a copy of the change with none of your users on it, and Your machine, staging, production is where that is set up. Small steps and a fast rollback are for the failures only production can reveal, not a substitute for trying it somewhere else first.
Migrations are different in kind. A A script that changes the structure of your database, such as adding a table or removing a column. It runs once, in order, against live data. runs against real rows. Redeploying old code does not put a dropped column back, and it does not recover values a script overwrote. Additive changes (a new table, a new nullable column) are safe because the old code ignores what it does not know about. Destructive changes are the dangerous ones, and the safe form is to split them across two deploys: stop using the column, ship, confirm nothing reads it, and remove it days later.
What to do
- Ship one change per deploy, and keep the commit message specific enough that you would recognise it in a list at 2am.
- Read the migration yourself, and read it for what it does to rows rather than for keywords. Any
statement beginning
UPDATE,DELETE,DROP,TRUNCATEorALTERcan touch data that already exists, and neither half of that group is the safe half:ALTER TABLE ... DROP COLUMNonly changes the shape and destroys every value in the column while doing it. For each statement, ask: which rows does this change, how many, and can the old values be recovered afterwards? ADROPis easy to spot. The one that actually catches people is anUPDATEwith a missingWHERE, which rewrites every row in the table and contains no alarming word at all. When the answer to "can I get the old values back" is no, that is the migration you take a fresh backup for. - Find out when your migrations actually run. On many setups they run automatically as part of the deploy, which means there is no pause between "I pushed" and "the database changed". Ask your assistant directly: "does anything in my deploy run migrations automatically, and where?" If the answer is yes, the reading and the backup above have to happen before you push, because the gap you were saving them for does not exist.
- Deploy when you can watch. Load the site yourself, check the error tracker for five minutes, and only then close the laptop. Friday evening is the traditional way to learn this rule.
Where it breaks
Small steps have a cost, and it is real. Some changes genuinely cannot be split: renaming a concept that runs through the whole app, or a framework upgrade that touches every route. Forcing those into tiny increments produces a half-migrated codebase, which is worse than either end state. Take the big change deliberately, at a quiet hour, with a backup and an hour set aside.
Rollback also has a limit nobody mentions. Once version B has written data in a format only B understands, going back to A means A reading rows it cannot parse. The window for a clean rollback closes the moment real users start creating data, which is usually minutes.