---
title: "Database Migrations as Code: Our Workflow"
description: "How we treat database schema changes with the same rigor as application code — version-controlled, reviewed, tested in staging, and reversible."
canonical: https://nowah.xyz/blog/database-migrations-as-code
lastModified: "2026-08-07T03:51:55.343Z"
---

# Database Migrations as Code: Our Workflow

How we treat database schema changes with the same rigor as application code — version-controlled, reviewed, tested in staging, and reversible.

"Just run this SQL in production." I have heard this sentence exactly twice in my career. Both times, it preceded a bad afternoon. The first time, a column got dropped that the application still referenced. The second time, a migration ran against the wrong database. Both times, the person who ran the SQL was experienced and careful. The process was the problem, not the person.

Database schema changes are the most consequential changes you can make to a system. Application code can be rolled back in seconds. A dropped column is gone, and the data with it. Treating schema changes with less rigor than application code is a category error that teams make until they learn the hard way.

## Schema-as-code

![Illustration for this section](https://pics.nowah.xyz/website-media/infrastructure-052-img-1.webp)

Our database schema lives in a single file that serves as the source of truth: the ORM schema definition. It's a declarative description of every table, every column, every relationship, and every index. When you want to know what the database looks like, you read this file. Not the database. Not a wiki page. The file.

The schema file is checked into version control alongside the application code. It goes through code review. It has a commit history. You can diff it between branches. You can see who changed what and when.

This might sound obvious, but it's remarkably common for teams to treat the database as a separate artifact from the code. Schema changes happen through admin consoles, through direct SQL execution, through migration scripts that aren't version-controlled. The database drifts from the code, and nobody notices until something breaks.

Our schema file defines all the models: users, traveler profiles, preferences, chat threads, messages, agent sessions, trips, bookings, documents, airports, airlines, [notification preferences](/blog/notification-preferences-done-right), and their relationships. It's a complete description of the data layer.

## Migration workflow

A schema change follows the same workflow as any code change.

The developer modifies the schema file. Maybe they're adding a column to the booking table, or creating a new table for a feature, or adding an index to improve query performance.

They run the schema push command locally. This applies the schema change to their local database and regenerates the type-safe client. Now the application code can reference the new column or table with full type checking. If the schema change breaks existing code, the compiler catches it immediately.

They open a pull request. The schema diff is visible in the review, alongside the application code that uses the new schema. Reviewers can see both the data change and the code change in one place. This is the key advantage: the reviewer doesn't have to context-switch between a migration script and the application code. They're in the same diff.

After approval, the change merges. The [deployment pipeline](/blog/deployment-pipeline-commit-production) applies the schema change to the staging database first, runs the test suite, and if everything passes, applies it to production.

## Automated execution in CI/CD

![Supporting diagram](https://pics.nowah.xyz/website-media/infrastructure-052-img-2.webp)

Schema changes run as part of the deployment pipeline, not as a separate manual step. The pipeline runs the schema push before starting the new application version. If the schema push fails, the deployment stops.

This automation eliminates an entire class of incidents: "the code deployed but nobody ran the migration." When schema changes are manual, they get forgotten. Someone deploys on a Friday afternoon and forgets the migration. The application crashes because the column it references doesn't exist yet. The on-call engineer spends an hour figuring out that the fix is running a migration.

With automated execution, the schema and the code always deploy together. The database is never behind the application and the application is never ahead of the database.

The type-safe client regeneration is also automated. After the schema push, the pipeline regenerates the client so that the deployed application has the correct types for the current schema. Type mismatches between the application and the database are caught at build time, not at runtime.

## Rollback strategies

Not every schema change is reversible. Adding a column is easily reversible (drop the column). Dropping a column is not (the data is gone). Renaming a column is somewhere in between (you can rename it back, but any code referencing the new name breaks).

We follow the expand-and-contract pattern for risky changes. Instead of renaming a column directly (which breaks the old application version), we add a new column (expand), deploy code that writes to both columns, deploy code that reads from the new column, then drop the old column (contract). Each step is independently reversible.

For additive changes (new columns, new tables, new indexes), rollback is straightforward: drop what was added. We include the rollback steps in the pull request description so that the on-call engineer knows how to undo the change if something goes wrong.

For destructive changes (dropping columns or tables), we don't actually drop immediately. We mark the column as deprecated in the schema, remove all code references, verify in production that nothing reads or writes the column, and only then drop it in a subsequent deployment. The gap between "stop using" and "actually drop" is at least one full deployment cycle.

## Schema drift detection

Schema drift is when the actual database doesn't match the schema file. This happens when someone makes a change directly in the database (through an admin console or a manual SQL command) without updating the schema file.

We detect drift by periodically comparing the schema file against the actual database schema. If they diverge, the build fails. This catches unauthorized changes quickly.

Drift also happens in the other direction: the schema file changes but the migration hasn't been applied to a particular environment. Our staging environment check verifies that the database matches the schema file before running tests. If staging has drifted, the tests fail with a clear error message rather than mysterious query failures.

The visual database browser helps developers verify schema changes visually. After applying a migration locally, they can open the browser and inspect the tables, columns, and data directly. This is particularly useful for complex migrations where seeing the result is more informative than reading the migration output.

## Adopt schema-as-code

If you're still running migrations manually or treating your database as a separate system from your code, here's the path forward.

Put your schema in a file. Whatever ORM or migration tool you use, the schema definition should be in version control. One file, one source of truth.

Automate migration execution. Schema changes should be part of the deployment pipeline. No manual steps. No "remember to run the migration." The pipeline handles it.

Regenerate your type-safe client automatically. If your language supports it, the types for your database access should be generated from the schema. Mismatches should be compile-time errors, not runtime crashes.

Review schema changes like code changes. Every schema change gets a pull request, a review, and a merge. The reviewer should see both the schema change and the application code that depends on it.

Test in staging before production. Always. The staging database should match production in structure (not data). The migration that works in staging is almost certainly going to work in production. The migration that was only tested locally might not.

Detect and prevent drift. Compare the schema file against the actual database regularly. Any divergence is a problem that should be fixed immediately, not ignored until it causes a [production incident](/blog/worst-production-incident).

---

Nowah is an AI travel agent that searches and books real flights and hotels through conversation — no filters, no thirty open tabs. [Plan your next trip](https://app.nowah.xyz).
