Many repos, one MongoDB database: migrations that don't step on each other
Most migration tools assume one app per database. That holds until it doesn't.
At work we're rolling mongo-migrate-kit out to about fifty repos. They don't have fifty databases. They have three, and one of them is shared by twenty repos across several products. Each repo has its own migrations/ folder, and they all point at the same database.
Put twenty repos on one database with a tool that assumes one app, and you don't get an error. You get something worse: things that look fine and aren't.
What goes wrong when repos share one history
Every repo writes its records to the same collection (_mmk_migrations). The tool can't tell whose record is whose, so:
statusshows other repos' migrations. Your table fills up with files that aren't in your repo.downcan undo another repo's work. "Roll back the last batch" means the last batch in the collection, which might belong to the repo that deployed five minutes before you.- A file name one repo applied counts as applied for all of them. Records are matched by name. If two repos ever use the same name (easy with
0001-init.jsstyle names), the second one never runs, and nothing tells you. - Batch numbers mix. Repo A gets batch 1, repo B batch 2, repo A batch 3. "Batch 2" means nothing to either of them.
None of these throw an error. Each one just waits for the wrong deploy.
How other tools deal with it
None of this is specific to MongoDB, so I looked at how tools in other ecosystems handle it:
| Approach | Who | How it works |
|---|---|---|
| One history table per app | Flyway, Knex, Mongock, golang-migrate, migrate-mongo, TypeORM | Each app gets its own history (and lock). Knex even refuses to run if the table has records for files it can't see. |
| An owner column in one shared table | Django (app + name), Liquibase (id + author + file) | One table, and every query includes "whose is this?" |
| One database per service | Prisma, Rails, Laravel | No sharing at all. It's an infrastructure decision. |
Separate tables are the most common answer. But with twenty repos on one database, that's twenty history collections, twenty locks, and no single place to see what ran. Django's approach scales better: one table, and every row says who owns it.
The fix: one line per repo
mongo-migrate-kit 1.5 does it the Django way. Each repo on a shared database gets a namespace:
// mmk.config.js in the orders-api repo
export default {
dbName: 'shop',
namespace: 'orders-api',
};That's all the setup there is. Every record now says which repo it belongs to, and every query looks only at its own:
All four problems go away:
mmk statusshows only this repo's migrations.mmk status --all-namespacesshows everyone's.mmk downundoes this repo's last batch, never another repo's.- Batch numbers count per namespace.
- Two repos using the same file name is an error, not a silent skip.
It doesn't add collections, however many repos join. Twenty repos is still one history and one lock.
Which setup do you have?
| Your setup | What to set |
|---|---|
| One repo, its own database | Nothing. You can ignore namespaces. |
| Several repos, one database, one team | namespace in each repo |
| Several products sharing a database | migrationsCollection per product and namespace per repo |
| A monorepo with several services | One namespace per service |
The rules that matter
- Treat the namespace as permanent. It decides which migrations count as applied. Change it and every migration looks pending. mmk notices and refuses to run (see the mistakes mmk catches).
- Put it in the repo's config file, not in an
MMK_NAMESPACEon a shared CI runner. That would give every repo built there the same one. - Every repo on the database needs 1.5 or newer. Older versions don't know about namespaces and see everyone's records.
- Keep timestamped file names (the default). Names must be unique within one history collection.
- MongoDB 4.2 or newer.
The rest of the series
This post is the starting point for a series on shared databases:
- One repo, one database? You can ignore namespaces
- Several repos, one MongoDB database, one team
- Several products in one MongoDB database
- Rolling out mongo-migrate-kit to 50 repos
- Adding a namespace to a repo that already ran migrations
- How locking works when 20 repos share one database
- The namespace mistakes mongo-migrate-kit catches for you
- MongoDB migrations in a monorepo
- Leaving migrate-mongo when several repos shared its changelog
serviceis deprecated, usenamespace
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
- Why I built a new MongoDB migration tool, where mongo-migrate-kit came from.
- Locking MongoDB migrations so two deploys can't collide, the lock every repo on a database shares.