Several repos, one MongoDB database, one team
Here's a common setup: one team, one MongoDB database, and a handful of repos that all change it. An API, a background worker, a notifications service. Each has its own migrations/ folder.
This post shows what that looks like with mongo-migrate-kit 1.5. All the output below is from real runs.
The setup: one line per repo
Three repos on the shop database. Each config is the same except for one line:
// orders-api/mmk.config.js
export default { dbName: 'shop', namespace: 'orders-api' };
// orders-worker/mmk.config.js
export default { dbName: 'shop', namespace: 'orders-worker' };
// notifications/mmk.config.js
export default { dbName: 'shop', namespace: 'notifications' };The connection string comes from MMK_URI in each environment, as usual. The namespace goes in the committed config file, because it belongs to the repo, like its name does.
Use lowercase letters, digits, ., _ or -. The repo name is the obvious choice.
Running migrations
Each repo runs mmk up as it always did. The only visible change is one extra line, Namespace:, near the top:
$ npx mmk up
Loaded config from mmk.config.js
Namespace: orders-api
✔ Applied 20261001090000-create-orders-indexes.js [1ms]
✔ Applied 20261001093000-backfill-order-totals.js [1ms]What's stored
All three repos write to the same _mmk_migrations collection. Each record now has a namespace:
{
name: '20261008110000-add-order-status-index.js',
namespace: 'orders-api',
batch: 2,
status: 'applied',
appliedAt: ISODate('2026-10-08T11:00:02Z'),
duration: 1,
checksum: '1ab36b6f9a83…',
environment: 'production',
executedBy: 'deploy',
author: 'Priya <priya@shop.dev>',
}Every question mmk asks the database (what's applied, what's pending, which batch is last) includes that namespace. Records without one belong to the default namespace, which is what you get when you don't set one.
Batch numbers are per repo
After orders-api deploys twice and the other two once each, the batches look like this:
| Namespace | Migration | Batch |
|---|---|---|
| notifications | 20261003120000-create-templates.js | 1 |
| orders-api | 20261001090000-create-orders-indexes.js | 1 |
| orders-api | 20261001093000-backfill-order-totals.js | 1 |
| orders-api | 20261008110000-add-order-status-index.js | 2 |
| orders-worker | 20261005080000-create-job-queue.js | 1 |
orders-api is on batch 2 and the others are on batch 1. So mmk down in orders-api undoes its own batch 2, never another repo's work. Without namespaces, those batches would have been numbered 1 to 4 across all three repos, and "the last batch" would belong to whoever deployed last.
Status: yours, or everyone's
mmk status shows only this repo's migrations:
$ npx mmk status
Loaded config from mmk.config.js
Namespace: orders-api
┌──────────────────────────────────────────┬─────────┬───────┬─────────────────────┬──────────┬────────────────────────┐
│ Migration │ Status │ Batch │ Applied At │ Checksum │ Author │
├──────────────────────────────────────────┼─────────┼───────┼─────────────────────┼──────────┼────────────────────────┤
│ 20261001090000-create-orders-indexes.js │ applied │ 1 │ 2026-10-01 09:00:02 │ ok │ Priya <priya@shop.dev> │
├──────────────────────────────────────────┼─────────┼───────┼─────────────────────┼──────────┼────────────────────────┤
│ 20261001093000-backfill-order-totals.js │ applied │ 1 │ 2026-10-01 09:00:02 │ ok │ Priya <priya@shop.dev> │
├──────────────────────────────────────────┼─────────┼───────┼─────────────────────┼──────────┼────────────────────────┤
│ 20261008110000-add-order-status-index.js │ applied │ 2 │ 2026-10-08 11:00:02 │ ok │ Priya <priya@shop.dev> │
└──────────────────────────────────────────┴─────────┴───────┴─────────────────────┴──────────┴────────────────────────┘When you want the whole picture, say during an incident, add --all-namespaces. It only reads:
$ npx mmk status --all-namespaces
Loaded config from mmk.config.js
┌───────────────┬──────────────────────────────────────────┬─────────┬───────┬─────────────────────┬────────────────────────┐
│ Namespace │ Migration │ Status │ Batch │ Applied At │ Author │
├───────────────┼──────────────────────────────────────────┼─────────┼───────┼─────────────────────┼────────────────────────┤
│ notifications │ 20261003120000-create-templates.js │ applied │ 1 │ 2026-10-03 12:00:01 │ Priya <priya@shop.dev> │
├───────────────┼──────────────────────────────────────────┼─────────┼───────┼─────────────────────┼────────────────────────┤
│ orders-api │ 20261001090000-create-orders-indexes.js │ applied │ 1 │ 2026-10-01 09:00:02 │ Priya <priya@shop.dev> │
├───────────────┼──────────────────────────────────────────┼─────────┼───────┼─────────────────────┼────────────────────────┤
│ orders-api │ 20261001093000-backfill-order-totals.js │ applied │ 1 │ 2026-10-01 09:00:02 │ Priya <priya@shop.dev> │
├───────────────┼──────────────────────────────────────────┼─────────┼───────┼─────────────────────┼────────────────────────┤
│ orders-api │ 20261008110000-add-order-status-index.js │ applied │ 2 │ 2026-10-08 11:00:02 │ Priya <priya@shop.dev> │
├───────────────┼──────────────────────────────────────────┼─────────┼───────┼─────────────────────┼────────────────────────┤
│ orders-worker │ 20261005080000-create-job-queue.js │ applied │ 1 │ 2026-10-05 08:00:01 │ Priya <priya@shop.dev> │
└───────────────┴──────────────────────────────────────────┴─────────┴───────┴─────────────────────┴────────────────────────┘Here's the same thing recorded, with a second repo (billing) on the database:
Repos take turns
The three repos share one history collection and one lock. So if orders-worker is in the middle of a deploy when orders-api starts, orders-api stops and tells you exactly who it's waiting for:
✖ LOCK_ALREADY_HELD: The migration lock is held by namespace "orders-worker"
Holder: host worker-7c9f, pid 41, user deploy, since 2026-10-15 19:09:17 (4s ago)
Why: all repos on this database take turns — one migration run at a time
If the holder crashed, the lock frees itself at 2026-10-15 19:10:17; `mmk unlock` releases it now (check the holder first)
Try: run again when it finishes (app start: runMigrations({ onLockHeld: "wait" }))In CI, re-run the job. The full story is in how locking works when 20 repos share one database.
Four rules
- Never change a repo's namespace. It decides what counts as applied. If it changes, mmk refuses to run instead of running everything again.
- Every repo on the database runs mongo-migrate-kit 1.5 or newer. Older versions see everyone's records.
- Keep the default
lockCollectioneverywhere. A repo with a different one stops taking turns. - Keep timestamped file names (the default). Names must be unique within the collection, and
sequentialnames like0001-init.jsclash between repos.
In short
One shared collection, one shared lock, and one line of config per repo. Each repo sees its own history, rolls back its own batches, and waits its turn.
Try it
npm install mongo-migrate-kit mongodb
npx mmk init --namespace orders-apiMore in the docs: Sharing a database. The package is on npm and GitHub.
Related reading
- Many repos, one MongoDB database: why shared histories break, and how other tools handle it.
- Several products in one MongoDB database: when one team becomes several.