Never edit a migration that has already run
Treat a shipped migration as immutable: fix it with a new one, never by editing it.
A migration is a record of what has already happened to a database somewhere. Editing one makes your repository disagree with production, and the disagreement is invisible until a fresh environment builds a schema nobody has ever run against. The cost lands on whoever next sets up the project, which is usually the newest person on the team.
What goes wrong: Production and a fresh checkout end up with different schemas, and the difference is discovered by a query that works on one.
You are violating it when
- A migration file's contents changed after it was merged.
- Setting up a fresh database produces a schema that differs from production.
- Migration filenames do not sort in the order they ran.
The usual objection: That editing is fine before release. Fine until one teammate has run it, and you rarely know that at the moment you are editing.
A migration is not just code that happens to change a database. It is also an audit trail of which shape the database took, in which order, on machines that may still exist. Once it has run anywhere shared, the file is part of the past. The correction belongs in a later migration that starts from the state real databases already reached.
This works because migration systems track a sequence: numbered files, revision parents, timestamps, or recorded versions in a schema table. Editing an old step creates two histories with the same name. Existing databases remember that the old step ran, while a new checkout replays the edited step and silently lands somewhere else. The bug often appears much later, when a query, index, constraint, or data assumption differs between environments.
The misconception is “editing is fine before release.” That assumes perfect knowledge of who has run the migration and where. In practice, a teammate, preview app, CI database, staging job, or local test environment may already have applied it. Release is not the boundary that matters; execution is.
When an agent is writing the code, this rule becomes more important because agents often prefer to modify the smallest visible file. A good review treats changes to old migration files as suspicious by default and asks for a new migration that moves the schema forward from the already-applied state.
Install it
npx klay practices add forward-only-migrations.klay/practices/forward-only-migrations.mdcreate# Forward-only migrations A migration is not source code. It is a record of something that has already happened to a database somewhere. Editing one makes your repository disagree with production, and the disagreement is invisible until a fresh environment builds a schema nobody has ever run against. The cost lands on whoever next sets the project up — usually the newest person on the team, who has no way to know the file lied. ## The rule Once a migration has been merged, it is immutable. Fix it with another migration.
The previews are the first lines of each file; the command writes them in full. Existing files are never overwritten.
How you know it stuck
npx klay practices audit reports this check for this practice:
migrations-forward-only
Where this comes from
- Tutorial — Alembic 1.19.1 documentationShows how revision chains make migration order and ancestry explicit in files.
- 5.7. Modifying TablesGrounds the rule in database reality: existing data is changed forward with ALTERs.
- Evolutionary Database DesignGives the canonical engineering framing for versioned, tool-applied database change history.
- Parallel ChangeAdds the safe rollout pattern for breaking schema changes without rewriting history.
- golang-migrate/migrateDemonstrates a common runner that operationalizes append-only migration files and recorded versions.
Questions
- What about a migration that is simply wrong?
- Write the migration that corrects it. The history should show the mistake and the fix — that is the record every other environment is going to replay.