Synopsis
up and refingerprint take no arguments; down requires a step count.
Anything else — no argument, a missing or non-positive count, an unknown flag, or
an unrecognised word — exits non-zero with a usage message.
Subcommands
On success each prints a confirmation and exits
0. On failure the error is
printed and the process exits non-zero, which is what makes it usable as an init
container or a deployment gate.
down needs a count, and guards the audit trail
migrate down refuses without a step count; there is no “all”. Before it
reverses anything it prints each migration in the plan and the DROP, DELETE
and ALTER TABLE … DROP statements in its down file:
users, user or workload identities — is
refused unless --force-drop-audit is given. The trail is evidence you may be
obliged to keep, and nothing restores it forward. The printed plan is re-checked
under the migration lock, so a concurrent migrate up makes the rollback refuse
rather than reverse something other than what was shown.
A down migration returns the schema, not the data it dropped. On any database
whose contents matter, take a backup first.
refingerprint after an identity change
A finding’s identity is a fingerprint computed from its stored identity
columns. When a release changes the rule that computes it, fingerprints already
in the database are stale — and a finding whose fingerprint moves would
otherwise be seen as new, losing the triage state, age and audit trail attached
to the old one.
migrate refingerprint recomputes them in place, carrying that history across:
migrate up, on any deployment taking a release whose notes
say the identity rule changed. It is idempotent and safe to repeat, and it is a
no-op on a deployment where nothing has moved — so running it when in doubt
costs a pass over the table and nothing else.
It is a migration command rather than something the application can invoke
because it runs as the migration role, which owns the schema and is exempt from
row-level security. That exemption is what lets a single pass cover every
tenant.
Read the report
The command prints a report on success and on failure — an interrupted run has still rewritten rows, and the count is what tells you where it got to. Two lines in it need a human:- Collisions. The corrected fingerprint already exists on the same asset, so the rewrite would merge two findings that each carry their own triage state. These are the duplicates the correction collapses, and which one survives is a decision the command will not make for you.
- Unrepairable. The stored columns cannot rebuild an identity, so the finding was left on its existing fingerprint.
Environment it reads
MIGRATIONS_PATH matters more than it looks. The default is relative, so
running the binary from a directory that is not backend/ finds no migrations.
In the container image the path is already correct; from source, run it from
backend/ or set the variable to an absolute path.
The binary also loads ../.env if present, which is what makes it work from
backend/ in development without exporting anything.
Running it
Throughmake, which handles the working directory:
Not from the published image
The container image ships the API and worker binaries and the migration files, but not themigrate binary itself. There is no
docker run … /app/migrate up.
So for a containerised deployment, run migrations one of these ways:
- Build a migration image from the same source tree, adding
cmd/migrateto the build stage and copying the binary into the final stage. It needs themigrations/directory, which the existing image already copies to/app/migrations. - Run
migrate upfrom a checkout with network access to the database, before the deployment rolls.
Where it belongs in a deployment
Runmigrate up to completion before starting the API or worker, not
alongside them. Both expect the schema they were built against, and a process
that starts against an older schema fails in whatever way the missing column
happens to produce.
The natural shapes are an init container that must exit 0 before the
application containers start, or a deployment-pipeline step gated on its exit
code. Do not run it as a sidecar.
Running it concurrently on several nodes will not corrupt the schema — the
underlying migration library takes an advisory lock in Postgres, so a second
runner waits rather than applying the same migration twice. That is a safety
net, not a design: run it once, from one place, then start everything else. See
Upgrades.
