Skip to content
Database migrations have a paved path: migrator

Database migrations have a paved path: migrator

September 28, 2026· DeploymentDatabase·Elliot Forbes
Elliot Forbes

Until now a Theseus service applied its schema at startup. It worked for a first table and nothing after: no record of what ran, no way back, and a schema that changed underneath the release still serving traffic. That gap is closed. Migrator is the standard way a Theseus service runs database migrations, and it is ready for every new service to adopt.

Migrator delivers Milestone 1 of the Theseus database migration epic. The shape was agreed in the September decision record, and Data Engineering has since confirmed the approach and its abstractions, with the longer-term design continuing in the database migration platform summary. That agreement is what moves this from pilot tooling to the paved path.

What you get

  • A contract, not a tool. Your repository carries db/migrations/ with one forward SQL file per version and a hand-written down/ counterpart for each. That directory is the API. What executes it is an internal detail of the migrator image and can change without you noticing.
  • Validation before anything touches a database. migrator validate runs offline and checks the pairing, naming, uniqueness, and that every file parses as PostgreSQL. It runs on every merge request in seconds.
  • A deploy step with a real answer. The migrations ship as an image derived from the migrator base, tagged with your release, and run as a Helm pre-upgrade hook shipped in your chart. Exit 0 means the schema is at the target version and the rollout proceeds. Anything else halts it: the new release is never applied, the old one keeps serving.
  • Run-once semantics under load. A database lock serialises concurrent runners. A re-run applies nothing. An applied migration that has been edited fails the run before anything is applied.
  • A rollback path. migrator down --to-version runs as a one-off Job from the newer release’s image, one transaction per step, resumable if it fails part-way. Irreversible migrations are declared explicitly and block rollback unless skipped on purpose. The runbook covers the procedure.

What to do

  • New service: follow Add database migrations to your service. It takes a scaffolded service through authoring, applying, rolling back, packaging, and wiring the migration hook in about thirty minutes.
  • Existing service with schema-at-startup: the same tutorial, plus the baseline step in the how-to, which records your current schema as applied without re-running it.
  • CI: add the validate and round-trip jobs from Run database migrations with migrator so bad SQL is caught on the merge request.

The Go service template will move to this pattern next so that scaffolded services start on it.

What is still landing

The hook is the working form today, not the final one. Fairway supports declaring init containers, but it does not yet give them the infrastructure mounts or the run-once semantics a migration needs, so a migration init container declared in fairway.yaml cannot reach the database on Runway. That is fairway#49. When it lands, moving over is deleting one template file and adding an initContainers entry to the manifest. The contract, the image, and the CI checks do not change, so adopting now costs nothing later.

Also on the roadmap: a shared CI component for the validation and round-trip jobs, Fairway injecting the migration step automatically when a Postgres dependency opts in, and Milestone 2, background jobs and long-running backfills.

Report problems and requests on the migrator project.

Last updated on