{"course":{"slug":"building-apps-with-ai","title":"Building Apps with AI","url":"https://addaly.com/learn/building-apps-with-ai","level":"beginner","track":"build"},"lesson":{"slug":"shipping-changes-without-breaking-what-works","title":"Shipping changes without breaking what works","url":"https://addaly.com/learn/building-apps-with-ai/shipping-changes-without-breaking-what-works","position":83,"of":87,"minutes":8,"answer":"A release is a routine, not an event: additive migration first, code second, cleanup in a later release, a flag for anything you might need to turn off.","keyIdea":"A release is a routine, not an event: additive migration first, code second, cleanup in a later release, a flag for anything you might need to turn off, and fifteen minutes watching the error tracker before you walk away."},"body":{"format":"markdown","text":"## The routine\n\nThe shipping block gave you the pieces: branches, previews, the version stamp, the rollback button. This lesson puts them in order, because the order is what keeps a release from being a gamble.\n\n1. Branch. Build the change with the agent, commit at each checkable step.\n2. Push. Open the pull request, read the diff as a whole, run the checks: build, typecheck, the three-account script, the eval set if the AI feature changed.\n3. Preview. Open the preview URL on your phone. Run the two-minute check on the changed feature and the stranger's checklist on the main path.\n4. Migrate, additively. If the schema changes, the migration adds; it never renames or drops in the same release as the code that stops using the old shape.\n5. Merge. The host deploys. Confirm the version stamp on the live site matches the commit.\n6. Watch. Fifteen minutes with the error tracker open and the daily-count query handy. Then walk away.\n\nSteps 4 and 6 are the ones people skip, and they are the ones that turn a small mistake into a visible outage.\n\n## Expand, then contract\n\nThe question above is the classic. A migration and a deploy are two events with a gap between them, and during the gap one version of the code runs against the other version of the schema. A rename breaks whichever version is on the wrong side.\n\nThe pattern that survives has three releases:\n\n**Expand.** Add `phone_number`. Deploy code that writes both columns and reads the new one, falling back to the old. A backfill copies old values across.\n\n**Switch.** Once every row has the new column and no old code is running anywhere, deploy code that only uses the new column.\n\n**Contract.** A later release drops `phone`.\n\nThree small releases, each of which works with the schema on both sides of its own gap, and any of which can be rolled back with the button. It feels slower and it is the only way that never produces the minute of errors. Put the rule in the rules file: migrations add; removals go in a separate, later migration.\n\n## A flag for anything you might turn off\n\nA new feature that turns out to be broken at 9pm should be switchable off without a deploy. The simplest flag is an environment variable read at request time (`FEATURE_AI_SUMMARY=off`) and a check in the route. The next step is a `flags` table with a name, an on-off value, and optionally a list of user ids or a percentage, so that a feature can go to you first, then ten percent, then everyone.\n\nA flag is not a permanent branch in the code. Once the feature has been on for everyone for a few weeks, remove the flag and the old path. Flags that live forever are the dead code the reading block warned about, with a switch attached.\n\n> Figure: Renaming a column without a minute of errors. Timeline: Release 1, Expand. Add phone_number. Code writes both columns and reads the new one, falling back to the old.; Between, A backfill copies every old value across. Nothing has been removed, so the rollback button is still safe.; Release 2, Switch. Every row has the new column and no old code is running; the code now uses only phone_number.; Release 3, Contract. Drop phone, in its own later migration.. A migration and a deploy are two events with a gap between them, and during the gap one version of the code runs against the other version of the schema. Each release here works on both sides of its own gap.\n\n## Deciding the rollback before you need it\n\nBefore merging, answer one question: what would make me press the rollback button? A new error type from the changed route, the daily count dropping, a report from the button. Write it in the pull request. Then when it happens at minute eight of the fifteen, the decision is already made and the button takes three seconds. The rollback lesson said the database does not roll back with the code; the expand-then-contract rule above is what makes the button safe to press.\n\n## When to ship\n\nNot at the end of the day, and not before a weekend, for a plain reason: the fifteen minutes of watching need a person, and the report that arrives at 11pm needs one too. Ship when you have an hour after it to be available. A release on Tuesday morning that breaks is a Tuesday morning problem; the same release on Friday evening is a weekend.\n\n## Telling users\n\nA line in the app, or a changelog page, or an email for the big ones: what changed, in their words rather than yours. \"You can now cancel a booking from the confirmation email\" rather than \"Added cancellation endpoint\". Users who know what changed report problems accurately, and a public changelog is the record you will search when a complaint says \"since last week\".\n\n## The whole routine takes an hour\n\nMost of it is waiting for builds and reading. It replaces the alternative, which is merging to main at midnight, deploying, and finding out in the morning. The routine is the difference between an app that a stranger can rely on and one whose users have learned to try again tomorrow."},"check":{"question":"A release renames the column phone to phone_number in one migration and deploys code that reads phone_number. Users report errors for about a minute after the deploy. Why, and what would have prevented it?","options":[{"text":"The build took a minute to propagate to the CDN; nothing would have prevented it short of a paid plan with faster deploys.","correct":false,"tempting":"Propagation delays affect static assets, not database columns; the errors came from code and schema disagreeing."},{"text":"For the minute between the migration and the new code going live, the old code was still reading phone, which no longer existed; adding the new column first and dropping the old one in a later release avoids that.","correct":true},{"text":"The migration should have run after the deploy rather than before it, so the new code was in place before the column changed; with the order reversed the new code would have found the renamed column ready and no request would have failed.","correct":false,"tempting":"Reversing the order gives a minute where the new code reads a column that does not exist yet; the fix is to never have a moment where either version cannot run."},{"text":"The rename should have been done by hand in the database editor to avoid the migration delay.","correct":false,"tempting":"A hand rename is instant and equally breaking for whichever version is running at that instant, and leaves the ledger wrong."}]},"prev":{"slug":"the-legal-minimum-of-a-public-site","title":"The legal minimum of a public site","url":"https://addaly.com/learn/building-apps-with-ai/the-legal-minimum-of-a-public-site"},"next":{"slug":"handing-it-to-a-real-developer","title":"Handing it to a real developer","url":"https://addaly.com/learn/building-apps-with-ai/handing-it-to-a-real-developer"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'Shipping changes without breaking what works', in Building Apps with AI. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/building-apps-with-ai/shipping-changes-without-breaking-what-works (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"Shipping changes without breaking what works","courseTitle":"Building Apps with AI","module":{"index":9,"slug":"what-it-costs-and-where-this-stops","title":"What it costs, running it, and where this stops"},"answer":null,"keyIdea":"A release is a routine, not an event: additive migration first, code second, cleanup in a later release, a flag for anything you might need to turn off, and fifteen minutes watching the error tracker before you walk away.","hero":{"kind":"figure","figure":{"t":"figure","label":"Figure","figure":{"type":"timeline","title":"Renaming a column without a minute of errors","events":[{"when":"Release 1","what":"Expand. Add phone_number. Code writes both columns and reads the new one, falling back to the old."},{"when":"Between","what":"A backfill copies every old value across. Nothing has been removed, so the rollback button is still safe."},{"when":"Release 2","what":"Switch. Every row has the new column and no old code is running; the code now uses only phone_number."},{"when":"Release 3","what":"Contract. Drop phone, in its own later migration."}],"caption":"A migration and a deploy are two events with a gap between them, and during the gap one version of the code runs against the other version of the schema. Each release here works on both sides of its own gap."},"alt":"Figure: Renaming a column without a minute of errors. Timeline: Release 1, Expand. Add phone_number. Code writes both columns and reads the new one, falling back to the old.; Between, A backfill copies every old value across. Nothing has been removed, so the rollback button is still safe.; Release 2, Switch. Every row has the new column and no old code is running; the code now uses only phone_number.; Release 3, Contract. Drop phone, in its own later migration.. A migration and a deploy are two events with a gap between them, and during the gap one version of the code runs against the other version of the schema. Each release here works on both sides of its own gap."}},"blocks":[{"t":"h2","id":"the-routine","runs":[{"text":"The routine"}]},{"t":"p","runs":[{"text":"The shipping block gave you the pieces: branches, previews, the version stamp, the rollback button. This lesson puts them in order, because the order is what keeps a release from being a gamble."}]},{"t":"ol","start":1,"items":[[{"text":"Branch. Build the change with the agent, commit at each checkable step."}],[{"text":"Push. Open the pull request, read the diff as a whole, run the checks: build, typecheck, the three-account script, the eval set if the AI feature changed."}],[{"text":"Preview. Open the preview URL on your phone. Run the two-minute check on the changed feature and the stranger’s checklist on the main path."}],[{"text":"Migrate, additively. If the schema changes, the migration adds; it never renames or drops in the same release as the code that stops using the old shape."}],[{"text":"Merge. The host deploys. Confirm the version stamp on the live site matches the commit."}],[{"text":"Watch. Fifteen minutes with the error tracker open and the daily-count query handy. Then walk away."}]]},{"t":"p","runs":[{"text":"Steps 4 and 6 are the ones people skip, and they are the ones that turn a small mistake into a visible outage."}]},{"t":"h2","id":"expand-then-contract","runs":[{"text":"Expand, then contract"}]},{"t":"p","runs":[{"text":"The question above is the classic. A migration and a deploy are two events with a gap between them, and during the gap one version of the code runs against the other version of the schema. A rename breaks whichever version is on the wrong side."}]},{"t":"p","runs":[{"text":"The pattern that survives has three releases:"}]},{"t":"p","runs":[{"text":"Expand.","b":true},{"text":" Add "},{"text":"phone_number","code":true},{"text":". Deploy code that writes both columns and reads the new one, falling back to the old. A backfill copies old values across."}]},{"t":"p","runs":[{"text":"Switch.","b":true},{"text":" Once every row has the new column and no old code is running anywhere, deploy code that only uses the new column."}]},{"t":"p","runs":[{"text":"Contract.","b":true},{"text":" A later release drops "},{"text":"phone","code":true},{"text":"."}]},{"t":"p","runs":[{"text":"Three small releases, each of which works with the schema on both sides of its own gap, and any of which can be rolled back with the button. It feels slower and it is the only way that never produces the minute of errors. Put the rule in the rules file: migrations add; removals go in a separate, later migration."}]},{"t":"h2","id":"a-flag-for-anything-you-might-turn-off","runs":[{"text":"A flag for anything you might turn off"}]},{"t":"p","runs":[{"text":"A new feature that turns out to be broken at 9pm should be switchable off without a deploy. The simplest flag is an environment variable read at request time ("},{"text":"FEATURE_AI_SUMMARY=off","code":true},{"text":") and a check in the route. The next step is a "},{"text":"flags","code":true},{"text":" table with a name, an on-off value, and optionally a list of user ids or a percentage, so that a feature can go to you first, then ten percent, then everyone."}]},{"t":"p","runs":[{"text":"A flag is not a permanent branch in the code. Once the feature has been on for everyone for a few weeks, remove the flag and the old path. Flags that live forever are the dead code the reading block warned about, with a switch attached."}]},{"t":"h2","id":"deciding-the-rollback-before-you-need-it","runs":[{"text":"Deciding the rollback before you need it"}]},{"t":"p","runs":[{"text":"Before merging, answer one question: what would make me press the rollback button? A new error type from the changed route, the daily count dropping, a report from the button. Write it in the pull request. Then when it happens at minute eight of the fifteen, the decision is already made and the button takes three seconds. The rollback lesson said the database does not roll back with the code; the expand-then-contract rule above is what makes the button safe to press."}]},{"t":"h2","id":"when-to-ship","runs":[{"text":"When to ship"}]},{"t":"p","runs":[{"text":"Not at the end of the day, and not before a weekend, for a plain reason: the fifteen minutes of watching need a person, and the report that arrives at 11pm needs one too. Ship when you have an hour after it to be available. A release on Tuesday morning that breaks is a Tuesday morning problem; the same release on Friday evening is a weekend."}]},{"t":"h2","id":"telling-users","runs":[{"text":"Telling users"}]},{"t":"p","runs":[{"text":"A line in the app, or a changelog page, or an email for the big ones: what changed, in their words rather than yours. “You can now cancel a booking from the confirmation email” rather than “Added cancellation endpoint”. Users who know what changed report problems accurately, and a public changelog is the record you will search when a complaint says “since last week”."}]},{"t":"h2","id":"the-whole-routine-takes-an-hour","runs":[{"text":"The whole routine takes an hour"}]},{"t":"p","runs":[{"text":"Most of it is waiting for builds and reading. It replaces the alternative, which is merging to main at midnight, deploying, and finding out in the morning. The routine is the difference between an app that a stranger can rely on and one whose users have learned to try again tomorrow."}]}],"check":{"question":[{"text":"A release renames the column phone to phone_number in one migration and deploys code that reads phone_number. Users report errors for about a minute after the deploy. Why, and what would have prevented it?"}],"options":[{"text":[{"text":"The build took a minute to propagate to the CDN; nothing would have prevented it short of a paid plan with faster deploys."}],"correct":false,"tempting":[{"text":"Propagation delays affect static assets, not database columns; the errors came from code and schema disagreeing."}]},{"text":[{"text":"For the minute between the migration and the new code going live, the old code was still reading phone, which no longer existed; adding the new column first and dropping the old one in a later release avoids that."}],"correct":true,"tempting":null},{"text":[{"text":"The migration should have run after the deploy rather than before it, so the new code was in place before the column changed; with the order reversed the new code would have found the renamed column ready and no request would have failed."}],"correct":false,"tempting":[{"text":"Reversing the order gives a minute where the new code reads a column that does not exist yet; the fix is to never have a moment where either version cannot run."}]},{"text":[{"text":"The rename should have been done by hand in the database editor to avoid the migration delay."}],"correct":false,"tempting":[{"text":"A hand rename is instant and equally breaking for whichever version is running at that instant, and leaves the ledger wrong."}]}],"order":[3,0,2,1],"why":null},"prev":{"slug":"the-legal-minimum-of-a-public-site","title":"The legal minimum of a public site","minutes":9},"next":{"slug":"handing-it-to-a-real-developer","title":"Handing it to a real developer","minutes":8}}}