Skip to main content
Handbook/Harden/Page 94 · Migrations

Changing the Database Safely

Share

Share this page

Pass it to someone who needs it.

Star on GitHub

Key takeaway: Change the schema safely

Your app is live and people are using it. Now the structure has to change: a new column on orders, a table that should be split in two, a field that should have existed from day one. The data those users already created is sitting in the database, so you cannot just reshape it and hope. This chapter gets you changing a live database's structure safely, the same controlled way every time, and undoing the change cleanly when it goes wrong.

10.5.1Every schema change is a versioned file

A migration is one change to your database's structure, written as a file and saved alongside your code. Add a column, rename a table, split one table into two: each becomes its own migration, numbered so the changes run in a fixed order.

That order is what makes them safe to trust. Your own machine, a test copy, and the live server all apply the same numbered files in the same sequence. Every one of them ends up with an identical structure.

Your agent writes these files and a migration tool applies them. Your stack almost certainly ships one already, Prisma Migrate in TypeScript, Alembic in Python, Rails and Laravel their own, with dbmate as the standalone option when yours has none. You never type the changes into the database yourself.

10.5.2Never edit the live database by hand

This is the one rule that keeps the whole system honest: never open the live database and change its structure by hand. Not a quick ALTER TABLE in a console, not a column added through a hosting dashboard.

A hand-edit exists in exactly one place, that single server. It is not in your code, so no one can review it and your test copy never receives it. The next time new code goes live, it has no idea the change ever happened. Structure changes only ever through a committed migration file, applied by the tool.

A hand-edit lives on one server and collides; a migration file is shared and safe.

Watch out: The costly database disasters almost always start with a well-meaning manual fix on the live server. If it did not go through a migration, it lives on that one box alone, and it will collide with your next release.

10.5.3Roll forward and roll back

Every migration has two halves. The up half rolls the change forward, applying it; the down half rolls it back, undoing it. When a change turns out bad, the tool runs the down half and the structure returns to exactly where it was.

-- db/migrations/20260720120000_add_order_status.sql -- migrate:up ALTER TABLE orders ADD COLUMN status text NOT NULL DEFAULT 'pending'; -- migrate:down ALTER TABLE orders DROP COLUMN status;

Your agent applies it with the tool's up command, dbmate up here, and reverses it with dbmate down, never by hand. The chapter on undoing a bad code change did this for your code; a migration does the same for your database's structure.

That one line is also the classic way to freeze a live app. On a big table, adding a NOT NULL column with a default can lock every read and write while it rewrites each row. Whether it does depends on your database and its version.

The safe shape there is three steps: add the column nullable, backfill in batches, then add the constraint. Ask your agent which of your migrations takes a lock, and never run one that does in the middle of a deploy.

One catch: some changes cannot be undone perfectly. Dropping a column throws its data away, so its down half cannot bring that data back. Make those one-way changes deliberately, and take a backup, a saved copy you can restore from, before you run them.

A reversible change undoes cleanly; one that drops data is one-way, so back up first.

Hand the change to your agent with the rules already set:

Ready prompt
Act as a senior engineer changing a live database safely. Read my data model and my spec first, then turn my schema change into a migration for the migration tool my stack already uses. Rules: - Write it as a versioned migration file, never a hand-edit to the live database. - Give it an "up" that applies the change and a "down" that reverses it. - If the change drops or rewrites existing data, warn me and tell me what to back up first. - Say whether it locks the table on a large one, and if so split it: nullable column, batched backfill, then the constraint. - Show me the up and down before anything runs, then apply it by command, not by hand. - Once applied, confirm the app still runs, update the data model in my spec to match, and log the change and its reason in my decision log. If you need the full reasoning behind this step, read https://zalt.me/guides/vibe-coding/harden/database-migrations The schema change I need:

Do this now: the next time your live app's structure has to change, paste the prompt with your change. Let your agent write the up-and-down migration instead of touching the database directly.

Mahmoud Zalt

Mahmoud Zalt

Software engineer, 16+ yrs · built Sistava.com in 3 months, idea to production, using these methods

Resources
Star on GitHubContribute
Donate

Support my work

A small tip keeps the free work coming.

© 2026 Mahmoud Zalt. Free to read, not to republish.
Copyright & license