mongo-migrate-kit
v1.2.2
Published
Elegant, fast, TypeScript-first MongoDB migrations for Node.js
Maintainers
Readme
mongo-migrate-kit
Elegant, fast, TypeScript-first MongoDB migrations for Node.js.
A modern, drop-in replacement for migrate-mongo etc.
Precise, safe migrations for MongoDB. Run a single file, roll back anything, and preview every change before it touches your database.
📖 Read the documentation →
Reasons to choose it
- Run a single migration —
mmk up <file>, not just "all pending". - Roll back anything — a batch (
--batch 3), the last N (--steps 2), one file, orredo. - Preview before you run —
mmk dry-run upprints the exact plan without touching the database. - No race conditions — an atomic MongoDB lock stops two deploys running migrations at once.
- Tamper detection — SHA-256 checksums catch a migration edited after it was applied.
- Audit trail kept — a rollback updates the record, it never deletes it.
- Lifecycle hooks —
beforeAll,afterAll,beforeEach,afterEach,onError. - Opt-in transactions — wrap a migration so it fully commits or fully aborts.
- TypeScript, ESM & CommonJS — all run with no
ts-nodeplumbing. - Zero config files required — drive everything from env vars if you prefer.
How it compares to migrate-mongo
| Capability | migrate-mongo | mongo-migrate-kit |
| ----------------------------------------------- | :-------------: | :-----------------: |
| Run a single migration file | ❌ | ✅ |
| Roll back a specific batch (not just the last) | ❌ | ✅ |
| Dry-run preview | ❌ | ✅ |
| redo (down + up) | ❌ | ✅ |
| Checksum / tamper detection | ❌ | ✅ |
| Lifecycle hooks | ❌ | ✅ |
| First-class TypeScript (built-in) | ❌ | ✅ |
| History kept on rollback (never deleted) | ❌ | ✅ |
| Adopt an existing migrate-mongo changelog | — | ✅ mmk import |
Reflects migrate-mongo's documented CLI as of mid-2026. It has since added transaction access
via a client argument; mmk exposes the same plus a declarative per-file useTransaction flag.
[!TIP]
🔄 Already using
migrate-mongo? Switch in under a minute.
mmkadopts your existingchangelogas-is — no re-running migrations, no data loss, no rewriting files. Point it at the same database and bring your whole history over in one command:mmk import # one-time: adopt your migrate-mongo changelog (it's never modified) mmk up # applies only what's new — your past migrations are recognized as already appliedYour applied history is preserved and new migrations run normally. Your
up/down/create/statusmental model carries over 1:1 — you just gain dry-runs, single-file control, real rollbacks, hooks, and locking. → See how it works
Quick start
npm install mongo-migrate-kit
npm install mongodb # required peer dependency# 1 · create a configuration file mmk.config.*. (pass --ts if need ts file)
npx mmk init
# 2 · create your first migration
npx mmk create "add users email index"
# 3 · run everything pending
npx mmk up
# 4 · see where you stand
npx mmk statusA migration is just an up and a down:
import type { MigrationContext } from 'mongo-migrate-kit';
export const description = 'Add unique index on users.email';
export async function up({ db }: MigrationContext): Promise<void> {
await db.collection('users').createIndex({ email: 1 }, { unique: true });
}
export async function down({ db }: MigrationContext): Promise<void> {
await db.collection('users').dropIndex('email_1');
}Prefer no files at all? Skip
mmk initand exportMMK_URIandMMK_DB— that is enough to run.
Documentation
Full docs, guides, and the API reference live at mongo-migrate-kit.vercel.app.
- Why mongo-migrate-kit? — how it compares to
migrate-mongo - Core Concepts — migrations, batches, the changelog, locking
- Getting Started & Tutorial
- Configuration · Writing Migrations · Transactions · Hooks
- Programmatic API · CI/CD · Troubleshooting
- Reference: CLI Cheatsheet · Error Codes
Commands
Every command accepts the global flags --uri, --db, --dir, and --config.
| Command | What it does |
|---|---|
| mmk init | Create a documented mmk.config.* in the current directory |
| mmk import | Adopt an existing migrate-mongo changelog (one-time, forward-only) |
| mmk create <name> | Generate a timestamped migration file |
| mmk up [file] | Run all pending migrations, or one named file |
| mmk down [file] | Roll back the last batch, a chosen batch, the last N steps, or one file |
| mmk redo [file] | Roll back then re-apply (the last migration, or one file) |
| mmk status | Print the full migration status table (--check to fail CI on pending) |
| mmk list | List migrations, filtered by status |
| mmk dry-run <up\|down> [file] | Preview a run without touching the database |
| mmk unlock | Force-release a stuck lock left behind by a crashed run |
Most data commands (up, down, redo, status, list, dry-run, import, create,
unlock) accept --json for machine-readable output — see CI & automation.
# init — generate a config file
mmk init # mmk.config.js (default)
mmk init --js # mmk.config.js (explicit default)
mmk init --ts # mmk.config.ts
mmk init --json # mmk.config.json (NOTE: here --json picks the file format)
mmk init --secret-provider # async config that loads the URI from a secret manager (js/ts only)
mmk init --force # overwrite an existing config file
mmk init --uri mongodb://localhost:27017 --db my_app # prefill the generated config
# import — adopt an existing migrate-mongo changelog
mmk import # read `changelog`, write the mmk changelog
mmk import --from <name> # read a differently-named source collection
mmk import --to <name> # write to a specific collection (default: config migrationsCollection)
mmk import --dry-run # preview the mapping, write nothing
mmk import --trust-hash # reuse migrate-mongo's fileHash instead of recomputing
mmk import --force # proceed even if the mmk changelog already has records
mmk import --no-lock # skip the concurrency lock (local dev only)
mmk import --json # machine-readable output
# create — generate a migration file
mmk create <name> # file type follows config `createExtension` (default .js)
mmk create <name> --ts # force a .ts file
mmk create <name> --js # force a .js file
mmk create <name> --template <path> # use a custom template
mmk create <name> --json # machine-readable output ({ "path": "..." })
# up — apply migrations
mmk up # all pending (one shared batch for the run)
mmk up <file> # one specific file
mmk up --step # apply each file as its own batch (revert individually later)
mmk up <file> --force # re-run an ALREADY-applied file (asks for confirmation)
mmk up <file> --force --yes # confirm a re-run non-interactively (required with --json)
mmk up --strict # abort on any checksum mismatch
mmk up --no-lock # skip the concurrency lock (local dev only)
mmk up --json # machine-readable output (array of run results)
# down — roll back
mmk down # the last batch (may be several files)
mmk down <file> # one specific file
mmk down --batch <n> # a specific batch number
mmk down --steps <n> # the last N migrations, newest first, ignoring batches
mmk down --no-lock # skip the concurrency lock (local dev only)
mmk down --json # machine-readable output (array of run results)
# redo — down then up
mmk redo # the most recently applied migration
mmk redo <file> # a specific file
mmk redo --json # machine-readable output (array of run results)
# status — full status table
mmk status # the full status table
mmk status --check # exit 1 if any migration is pending (CI gate)
mmk status --json # machine-readable output (array of status rows)
# list — filtered status
mmk list # all migrations
mmk list --pending # only pending
mmk list --applied # only applied
mmk list --json # machine-readable output (array of status rows)
# dry-run — preview, never writes
mmk dry-run up [file]
mmk dry-run down [file]
mmk dry-run down --steps <n> # preview a step rollback (the last N migrations)
mmk dry-run up --json # machine-readable output (array of status rows)
# unlock — clear a stuck lock after a crash
mmk unlock # shows the holder, prompts y/N
mmk unlock --yes # skip the prompt
mmk unlock --json # machine-readable output ({ "released": ..., "holder": ... })Global flags (available on all commands): --uri <uri> (override MMK_URI),
--db <name> (override MMK_DB), --dir <path> (override MMK_MIGRATIONS_DIR),
--config <path> (explicit config file, overrides auto-discovery).
--json is accepted by every data command above (up, down, redo, status, list,
dry-run, import, create, unlock) and prints one JSON document to stdout — see
CI & automation. On mmk init only, --json instead selects the config
file format (mmk.config.json).
Advanced features
mmk import reads your existing migrate-mongo changelog and records that history in the mmk
changelog, so mmk up knows what is already applied and runs only what is new. It is a one-time,
forward-only step.
# point mmk at the same database, then:
mmk import --dry-run # preview the mapping first (writes nothing)
mmk import # adopt the history
mmk up # apply only the migrations added sinceWhat it does
- Reads the source collection (
changelogby default;--fromto override) and never modifies it — the mapped records are written to themmkchangelog (your config'smigrationsCollection,_mmk_migrationsby default;--toto write to a different collection). - Maps
fileName → name,appliedAt → appliedAt, and resolves a checksum: it reusesmigrate-mongo'sfileHashwhen it matches the file on disk, otherwise recomputes a SHA-256 from disk (--trust-hashreuses the stored hash as-is). Records whose files are missing are still imported. - Assigns each migration a unique, sequential batch number in apply order. If the
mmkchangelog already has records, imported batches continue after the existing maximum (use--forceto import into a non-empty changelog). - Leaves migration files that exist on disk but are not in the source changelog pending — they
run on the next
mmk up, exactly as expected for newly added migrations.
Options
| Flag | Default | What it does |
|---|---|---|
| --from <collection> | changelog | Source collection to read (never modified). |
| --to <collection> | config migrationsCollection (_mmk_migrations) | Target collection to write the adopted history to. |
| --dry-run | off | Preview the mapping and print the table; writes nothing. |
| --trust-hash | off | Reuse migrate-mongo's stored fileHash as-is instead of recomputing the checksum from disk. |
| --force | off | Import into a changelog that already has records (imported batches continue after the existing max). |
| --no-lock | off | Skip the MongoDB concurrency lock (local dev only). |
Plus the global flags --uri, --db, --dir, and --config.
Forward-only — imported migrations cannot be rolled back
Adopted records are tagged origin: 'migrate-mongo'. migrate-mongo files use a positional
up(db, client) signature, which mmk does not execute (it passes a single context object). To avoid
ever corrupting your data, mmk down / mmk redo refuse an imported migration up front, before
running or writing anything, and tell you why:
✖ Cannot roll back 1 migrate-mongo-imported migration(s): 20260101-add-index.jsIf you need an old migration to be reversible under mmk, re-author its file in the native format
(named exports, single context argument — see Migration file formats).
Opt in per file with export const useTransaction = true (or globally via config). The runner opens a
session, passes it through the context, and commits on success or aborts on any error. Pass the
session to every operation so it joins the transaction:
export const useTransaction = true;
export async function up({ db, session }: MigrationContext): Promise<void> {
await db.collection('accounts').insertOne({ balance: 100 }, { session });
await db.collection('ledger').insertOne({ delta: 100 }, { session });
}Transactions require a replica set or sharded cluster — MongoDB's own requirement, not a library limit.
Define hooks in your config file. Use them to seed data, emit metrics, or alert on failure:
hooks: {
beforeAll: async (ctx) => { /* once, before the batch */ },
afterAll: async (ctx) => { /* once, after the batch */ },
beforeEach: async (name, ctx) => { /* before each file */ },
afterEach: async (name, durationMs, ctx) => { /* after each file */ },
onError: async (name, error, ctx) => { /* a file threw — alert, then it re-throws */ },
}A .ts/.js config may export a function (sync or async) instead of an object.
mmk calls it once per command, so you can fetch the connection from a secret manager at run time.
The secret is never written to disk, and a rotated value is picked up automatically on the next run.
The library ships no cloud SDKs — you bring the one you already use, so any provider works:
// mmk.config.js — AWS Secrets Manager
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
export default async () => {
const sm = new SecretsManagerClient({ region: 'us-east-1' });
const res = await sm.send(new GetSecretValueCommand({ SecretId: 'prod/mongo' }));
const { uri, dbName } = JSON.parse(res.SecretString ?? '{}');
return { uri, dbName }; // merged at the config-file tier — env vars / flags still override
};Run mmk init --secret-provider to scaffold this form with an AWS example you can swap for any provider.
If the function throws, it surfaces as a ConfigInvalidError with the cause attached.
A batch is one mmk up run. By default every migration applied in a single run shares one batch
number, so mmk down rolls back that whole run as a unit — the same model used by Laravel and
Knex. That keeps a deploy atomic: one command applied it, one command reverts it.
When you want finer control, two flags mirror Laravel's migrate --step / migrate:rollback --step:
mmk up --step— apply each file in the run as its own sequential batch instead of one shared batch. A latermmk downthen peels them off one at a time.mmk down --steps <n>— revert the last N applied migrations, newest first, counted as individual files regardless of batch.mmk down --steps 1reverts just the single most-recently applied migration; a larger N can cross batch boundaries, so preview it first withmmk dry-run down --steps <n>.
--steps is mutually exclusive with --batch and a filename. Migrations are always reverted
newest-first, so up followed by down --steps <same n> returns you to the starting state.
Lock. Each run acquires an atomic lock document in _mmk_locks, so two deploys can never migrate
at once. A lock older than lockTTLSeconds is treated as stale and reclaimed; while a migration runs,
a heartbeat renews the lock at half the TTL so a long migration can't have its lock stolen mid-run.
The lock is always released in a finally block. --no-lock bypasses it for local development (and
warns loudly). If a process crashes hard and leaves a lock behind, clear it with mmk unlock (it
shows you who held it and asks for confirmation).
Checksums. Every applied migration stores a SHA-256 of its file. On later runs mmk compares the
two and surfaces drift in status. With strict: true (or --strict) a mismatch aborts the run;
otherwise it warns and skips. To intentionally re-run an edited, already-applied file, use
mmk up <file> --force.
Machine-readable output. Add --json to any data command (up, down, redo, status,
list, dry-run, import, create, unlock) to get a single JSON document on stdout — all
human logs and the spinner are redirected to stderr, so the stream is safe to pipe into jq or parse
in a script. On failure the command prints { "error": { "code": "...", "message": "..." } } to
stdout and exits 1.
# Apply pending migrations and capture the result in CI
mmk up --json | jq '.[] | select(.status == "applied") | .file'
# Fail a deploy step if the database isn't fully migrated
mmk status --check # exits 1 when anything is pending, 0 otherwise
# Inspect status as data
mmk status --json | jq 'map(select(.status == "pending")) | length'A typical pipeline gate:
# .github/workflows/deploy.yml (excerpt)
- name: Fail if migrations are pending
run: npx mmk status --check --uri "$MONGO_URI" --db "$MONGO_DB"Note:
mmk init --jsonis the one exception — there--jsonmeans "writemmk.config.json", not machine-readable output (kept for backwards compatibility).
Every record in _mmk_migrations stores batch, status, appliedAt, revertedAt, duration,
checksum, environment, and executedBy. Rolling back updates a record's status to reverted
and stamps revertedAt — it is never deleted, so the full history stays intact for compliance.
Every CLI command is a method on MigratorKit:
import { MigratorKit } from 'mongo-migrate-kit';
const migrator = new MigratorKit({
uri: 'mongodb://localhost:27017',
dbName: 'my_app',
migrationsDir: './migrations',
});
await migrator.connect();
const applied = await migrator.up(); // RunResult[]
const rows = await migrator.status(); // StatusRow[]
await migrator.disconnect();All errors extend MmkError and carry a typed code (LOCK_ALREADY_HELD, CHECKSUM_MISMATCH,
NOT_APPLIED, …), so catch blocks stay type-safe.
Configuration
mmk resolves settings in this order (highest wins):
CLI flags → environment variables → config file → built-in defaults
A config file is optional and auto-discovered in the working directory as mmk.config.ts,
mmk.config.js, or mmk.config.json. Run mmk init to generate one — it ships fully commented,
so every setting lives in one documented place:
// mmk.config.js — generated by `mmk init`, every option explained
/** @type {import('mongo-migrate-kit').MmkConfig} */
export default {
// ── Connection (required) ───────────────────────────────────────────────
uri: 'mongodb://localhost:27017', // MongoDB connection string
dbName: 'my_app', // database to run migrations against
// ── Files ───────────────────────────────────────────────────────────────
migrationsDir: './migrations', // where migration files live
fileExtensions: ['.ts', '.js'], // which files count as migrations
createExtension: 'js', // default type for `mmk create` ('js' | 'ts'); --js/--ts override
sequential: false, // true → 0001-style numbering instead of timestamps
// templatePath: './migration.template.ts', // custom template for `mmk create`
// ── Bookkeeping collections ─────────────────────────────────────────────
migrationsCollection: '_mmk_migrations', // the append-only audit trail
lockCollection: '_mmk_locks', // the concurrency lock
lockTTLSeconds: 60, // a lock older than this is reclaimable
// ── Safety ──────────────────────────────────────────────────────────────
strict: false, // true → abort on a checksum mismatch (instead of warn + skip)
useTransaction: false, // true → wrap every migration in a transaction (override per file)
// ── Code-only options (omit in mmk.config.json) ─────────────────────────
// hooks: { beforeAll, afterAll, beforeEach, afterEach, onError },
// mongoose: myMongooseInstance, // pass if your migrations use Mongoose models
// logger: null, // null silences all output (handy in CI/tests)
};| Env var | Config key | Default |
|---|---|---|
| MMK_URI | uri | — (required) |
| MMK_DB | dbName | — (required) |
| MMK_MIGRATIONS_DIR | migrationsDir | ./migrations |
| MMK_COLLECTION | migrationsCollection | _mmk_migrations |
| MMK_LOCK_COLLECTION | lockCollection | _mmk_locks |
| MMK_LOCK_TTL | lockTTLSeconds | 60 |
| MMK_STRICT | strict | false |
| MMK_USE_TRANSACTION | useTransaction | false |
| MMK_SEQUENTIAL | sequential | false |
| MMK_CREATE_EXTENSION | createExtension | js |
.env files are loaded automatically.
Migration file formats
mmk loads TypeScript and both JavaScript module systems with no extra setup:
// TypeScript / ESM — named exports (native on Node 22.18+, or under a loader like tsx)
export async function up({ db }) { /* ... */ }
export async function down({ db }) { /* ... */ }// CommonJS — default export
module.exports = {
async up({ db }) { /* ... */ },
async down({ db }) { /* ... */ },
};Optional per-file exports: description (shown in status) and useTransaction. Note that up/down
receive a single context object ({ db, client, mongoose?, session? }) — not migrate-mongo's
positional (db, client).
ESM vs CommonJS: Node decides a file's module system from its extension and the nearest
package.json"type". In a project with"type": "module", a.jsfile is an ES module, somodule.exports = …throws "module is not defined in ES module scope." Use namedexports (above), or name the file.cjsand add'.cjs'tofileExtensionsin your config.
License
MIT © guptasantosh327
