Skip to content

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:

ts
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 });
}
js
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.

js
// 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:

  1. A MongoDB session starts and session.startTransaction({ writeConcern: { w: 'majority' } }) is called.
  2. The session is exposed as ctx.session to your up/down.
  3. On success → the changelog record is written through the same session, then session.commitTransaction().
  4. On any thrown error → session.abortTransaction(), the onError hook 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.

Released under the MIT License.