Database migrations have a paved path: migrator
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-writtendown/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 validateruns 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-versionruns 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
validateand 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.