Transactions
mongo-migrate-kit can wrap a migration in a MongoDB session + transaction so that either every operation commits, or none do. This is opt-in and works at two levels.
Requires a replica set
MongoDB transactions require a replica set (or a sharded cluster). A standalone mongod does not support them. Local development with mongodb-memory-server spins up a replica set automatically.
mmk detects a standalone deployment when it connects and fails fast with a TransactionsUnsupportedError (TRANSACTIONS_UNSUPPORTED) before the migration body runs — so a transaction on the wrong topology errors immediately instead of half-way through at commit time.
Enable per file
Export useTransaction = true from a migration:
import type { MigrationContext } from 'mongo-migrate-kit';
export const useTransaction = true;
export async function up({ db, session }: MigrationContext): Promise<void> {
await db.collection('accounts').updateMany({}, { $inc: { balance: 0 } }, { session });
await db.collection('ledger').insertOne({ migratedAt: new Date() }, { session });
}
export async function down({ db, session }: MigrationContext): Promise<void> {
await db.collection('ledger').deleteMany({}, { session });
}export const useTransaction = true;
export async function up({ db, session }) {
await db.collection('accounts').updateMany({}, { $inc: { balance: 0 } }, { session });
await db.collection('ledger').insertOne({ migratedAt: new Date() }, { session });
}Always pass session
For operations to participate in the transaction, you must pass ctx.session to each driver call. An operation without { session } runs outside the transaction and won't be rolled back.
Enable globally
Set useTransaction: true in your config to wrap every migration in a transaction by default. A per-file useTransaction still overrides the global setting.
// mmk.config.js
export default {
uri: 'mongodb://localhost:27017',
dbName: 'my_app',
useTransaction: true, // every migration is transactional unless it opts out
};How it behaves
When a transactional migration runs:
- A MongoDB session starts and
session.startTransaction({ writeConcern: { w: 'majority' } })is called. - The session is exposed as
ctx.sessionto yourup/down. - On success → the changelog record is written through the same session, then
session.commitTransaction(). - On any thrown error →
session.abortTransaction(), theonErrorhook fires, and the batch stops.
This means a failed transactional migration leaves the database in its original state — no partial writes.
The changelog record is part of the transaction
Because the "applied" record is written inside the transaction (step 3), the migration's data and its bookkeeping record commit atomically. There is no window where the data is committed but the record is missing — the gap that would otherwise let a non-idempotent migration re-run on the next deploy after a crash between commit and recording. For non-idempotent migrations against production, enabling useTransaction is the way to get exactly-once semantics.