How it works
You can use mmk without reading this page. But ten minutes here will make every error message and every flag make sense, and you'll know what's going on when something goes wrong in production.
Migrations and the history collection
A migration is a file with an up function and a down function. That's all mmk needs from you.
mmk keeps track of what has run in a collection in your database called _mmk_migrations. Each migration that has run gets one record. Here's a real one, from the tutorial:
{
name: '20261015223652-add-user-status.js',
batch: 1,
status: 'reverted', // 'applied' while it's in effect
appliedAt: ISODate('2026-10-15T17:09:58.069Z'),
revertedAt: ISODate('2026-10-15T17:09:58.213Z'),
duration: 1, // milliseconds
checksum: 'f14593fff4ae45172a4847290917f42ad36ca84bcdedf5327ba98b1cf0f171e7',
environment: 'production', // NODE_ENV when it ran
executedBy: 'asha', // the OS user who ran it
author: 'Asha Rao', // who wrote it, from the file
description: 'Give every user a status',
}Two things to take away:
- This collection is how mmk knows what's done. If a file has an
appliedrecord, it won't run again. If it doesn't, it's pending. - Records are never deleted. Rolling back sets
statustorevertedand addsrevertedAt. When something breaks at 2am, you can see exactly what ran, when, and who wrote it.
A second collection, _mmk_locks, holds the lock while a run is in progress. It's empty the rest of the time. You don't need to create either one. mmk does that the first time it runs.
How mmk decides what to run
When you run mmk up, mmk:
- takes the lock, so nobody else runs migrations at the same time,
- lists the files in your migrations folder and compares them with the records,
- runs every file that has no
appliedrecord, oldest first (by file name), and saves a record after each one, - releases the lock. It always does this, even if a migration failed.
If a migration throws, the run stops right there. Files after it don't run, and the failed file stays pending so you can fix it and run mmk up again.
Why file names matter
Order comes from the file name, and that's why mmk create puts a timestamp at the front (20261015223652-add-user-status.js). Everyone on the team gets the same order, whatever order they pulled the files in.
Two rules follow from this:
- Don't rename a file that has run anywhere. The record is keyed by file name, so a renamed file looks like a new, pending migration.
- Don't edit a file that has run anywhere. Write a new migration instead. (More on this under checksums.)
Batches
Every mmk up run gets a batch number. If one run applies three files, all three share the same batch. Think of a batch as "what one deploy changed".
Batches matter when you roll back, because mmk down works on batches by default:
mmk downundoes the last batch. That's usually what you want after a bad deploy: one command undoes everything that deploy did.mmk down --batch 2undoes one batch by number, even if it isn't the latest.mmk down --steps 3ignores batches and undoes the last three files, newest first.mmk down <file>undoes one file.
Files are always undone newest first. Here's a real session: two batches, undo the last one, then one more file with --steps:
If you'd rather undo files one at a time, run mmk up --step. It gives each file its own batch, so a plain mmk down undoes only the newest one.
The lock
Picture two deploys starting a few seconds apart, or three app instances booting at once. Without anything to stop them, they'd all see the same pending migration and all try to run it.
So before it writes anything, mmk takes a lock: one document in _mmk_locks. Taking it is a single atomic operation in MongoDB, so only one process can win. Anyone else stops straight away and is told who has it:
A few details that matter in production:
- It expires. If a process is killed and never releases the lock, the lock goes stale after
lockTTLSeconds(60 by default) and the next run can take it. - Long migrations keep it. While a migration runs, mmk renews the lock every half TTL, so a 20-minute backfill doesn't lose its lock halfway through.
- You can see who holds it.
mmk lockshows the holder and whether its lock has gone stale, and changes nothing. - You can clear it by hand.
mmk unlockshows who holds it and asks before removing it. - Read-only commands don't take it.
status,listanddry-runnever wait for the lock.
If your app runs migrations on startup, runMigrations({ onLockHeld: 'wait' }) makes the other instances wait their turn instead of failing.
Checksums
When a migration runs, mmk saves a SHA-256 checksum of the file. Later it compares the file on disk with that checksum. If they differ, someone edited a migration after it ran.
That's a problem because the database was changed by the old code. The new code describes something that never happened. Here's what it looks like:
mmk statusshowsMISMATCHin the Checksum column.mmk downrefuses to run an edited file'sdown()and tells you how to proceed. Rolling back with code that doesn't match what ran is the risky direction, so this check is always on.mmk uponly looks at pending files, so it won't notice an edit to a file that already ran. That's whymmk statusis the place to look. (If you name an applied file, as inmmk up <file>, mmk warns and skips it, or stops ifstrictis on.)
The fix is almost always the same: put the old file back (git checkout), and make the change in a new migration. If you really mean it, --force gets you past the check.
When a migration fails
The run stops at the failed file and mmk tells you what happened: the error, the line in your file that threw it, and whether the writes it made are still in the database.
That last part depends on whether the migration ran in a transaction. Without one, any writes made before the error stay. With export const useTransaction = true, they're rolled back together with the record. See Transactions.
Summary
| Thing | Where it lives | What it's for |
|---|---|---|
| Migration files | migrations/ in your repo | The changes, in name order |
| History | _mmk_migrations collection | Which files ran, when, by whom; never deleted |
| Lock | _mmk_locks collection | One run at a time per database |
| Checksum | on each history record | Notices files edited after they ran |
| Batch | on each history record | Groups one run, so one down can undo it |
Next
Writing migrations covers the file format in full and the habits that make migrations easy to roll back. Using TypeScript? See TypeScript too.