Writing Database Migrations That Are Safe to Run on Every Deploy

By Sharon C
Aug 19, 2026 4 min read 7 views
Writing Database Migrations That Are Safe to Run on Every Deploy
Database changes run automatically with every release. Here is how we write migrations that are additive, guarded and safe for a database many customers share.
Share:

Our releases run database migrations automatically. That keeps the database and the code in step, and it means database changes are reviewed and recorded like any other change. It also means every migration runs against the shared database that serves every organization on the platform, during a live release.

That raises the stakes. A careless migration can lock a busy table, drop data that is still needed or change the wrong account's records. Here are the habits we use to keep migrations safe.

Prefer additive changes

The safest schema changes add rather than remove: a new table, a new column, a new index. Existing code keeps working because nothing it uses has disappeared. New columns are usually nullable or have sensible defaults, so existing rows remain valid.

Removing or renaming something is done in steps across releases: first stop using it in code, then remove it once nothing depends on it. That way no release ever runs code that expects something the database no longer has.

Separate schema changes from data changes

Changing a table's structure and changing the data inside it are different kinds of work. When a new column needs values for existing rows, we treat that backfill as its own step, written to work in batches and to be safe if it runs again. Mixing a large data update into a structural change makes both harder to reason about.

Quote card: A migration runs against every customer's data at once. Write it like it will be read by someone who cannot undo it.

Scope every data change explicitly

Data migrations run outside a normal web request, so the automatic protections that keep each organization's data separate may not apply. Every data change names its scope explicitly: which account, which website, which records. A migration that sets a default for one organization must never quietly change every organization's rows.

Guard one-time data work

Some migrations deliver content or fix data for a specific account, such as publishing a set of pages. Those migrations check their assumptions before writing: the account exists, its identity matches what is expected and the work has not already been done. If anything is wrong, they stop with a clear error. We describe that pattern in one-time content seeders that respect later edits.

Mind the database's limits

Our database does not wrap structural changes in transactions the way some databases do, so a structural migration that fails partway cannot simply roll back. We keep structural migrations small and focused, test them before release and avoid combining many risky changes in one file.

Large tables need extra care. Adding an index or changing a column on a big table can take time and affect performance during a release, so those changes are planned with the table's size in mind.

Reversals are not always safe

Migrations can define how to undo themselves. For structural changes, that is useful. For data deliveries, an automatic undo that deletes content could destroy edits an owner made after the migration ran. In those cases, the undo step intentionally does nothing, and any real reversal is a deliberate, reviewed decision with a fresh backup.

Check before shipping

Because a failed migration fails the release, we check migrations before they ship: a syntax check against the production language version and, for data migrations, a dry run that reads real data and reports what would change without writing anything. Those checks are read-only and catch most problems before they reach a release.

Migrations are reviewed like code

Migrations are reviewed with the same care as application code, often more. Reviewers check that changes are additive, that data updates are scoped and repeatable, that indexes exist for the queries that will use new columns and that nothing in the migration depends on a particular order of rows or on data that might not exist in every environment.

Composite keys and indexes from the start

New tables that hold customer data include the account and website columns from the first migration, with uniqueness rules scoped to them, as described in composite unique keys. Adding those later is much harder than including them at the start.

Frequently asked questions

Is it safe to run migrations automatically on deploy?

It can be, when migrations are additive, small, explicitly scoped and checked before release, and when risky reversals are avoided.

How do you remove a database column safely?

Stop using it in code first, release that change, then remove the column in a later release once nothing depends on it.

Why separate data backfills from schema changes?

Backfills can be large and should run in batches safely, while schema changes should stay small and focused. Separating them keeps both easier to reason about.

Comments (0)

No comments yet. Be the first to share your thoughts!