Before launch, a mistake costs you an hour. After launch, a mistake costs your users their afternoon, and you find out about it in public.

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.

code
   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 is redeploying the previous commit or restarting the old release directory. Do it once deliberately while everything is fine, so you know the button, the command, and how long it takes.

Migrations are different in kind. A 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

  1. Ship one change per deploy, and keep the commit message specific enough that you would recognise it in a list at 2am.
  2. Before any migration, take a backup and read the SQL yourself. Look for DROP, DELETE, TRUNCATE and ALTER COLUMN ... TYPE. Those four words are the whole risk. Ask Claude Code to explain what each statement does to existing rows.
  3. 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.