Skip to content

Writing Migrations ​

A migration file exports an up function (apply the change) and a down function (revert it). Both receive a typed MigrationContext. All three module formats are first-class.

Indexes and validators

A migration is the right tool when a change must run once, in order, against your data. When all that matters is the end state of a collection's indexes and validator, declare it and run migronaut converge instead.

The three supported formats ​

ts
import type { MigrationContext } from '@alexify/migronaut';

export const description = 'Add unique index on users.email';
export const useTransaction = true;

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');
}
js
export const description = 'Add unique index on users.email';

export async function up({ db }) {
  await db.collection('users').createIndex({ email: 1 }, { unique: true });
}

export async function down({ db }) {
  await db.collection('users').dropIndex('email_1');
}
js
module.exports = {
  description: 'Add unique index on users.email',
  async up({ db }) {
    await db.collection('users').createIndex({ email: 1 }, { unique: true });
  },
  async down({ db }) {
    await db.collection('users').dropIndex('email_1');
  },
};

Generate the scaffold

Don't write the boilerplate by hand — run migronaut create add-users-email-index. The generated file type follows your createExtension config, or pass --ts / --js to choose.

The migration context ​

Every up/down receives a single context object:

ts
interface MigrationContext {
  /** Native MongoDB Db instance */
  db: Db;
  /** Native MongoClient — use for sessions / transactions */
  client: MongoClient;
  /** Mongoose instance — only present if passed in config */
  mongoose?: Mongoose;
  /** Active session — present when the migration runs in a transaction */
  session?: ClientSession;
  /** Aborted when the run is stopping: the lock was lost, stop(), SIGTERM, a timeout */
  signal?: AbortSignal;
  /** The kit's logger, bound to this run — see Migration Logs */
  logger?: MigronautLogger;
  /** The run id, migration, direction, batch, attempt, queue job and actor — frozen */
  run?: MigrationRunInfo;
}

A long migration should watch signal: migronaut cannot interrupt a running function, but it can tell it to stop. logger and run are always there when migronaut runs the file (optional in the types only so that a context built by hand in a test type-checks):

ts
export async function up({ db, logger }: MigrationContext): Promise<void> {
  const { modifiedCount } = await db
    .collection('users')
    .updateMany({ plan: { $exists: false } }, { $set: { plan: 'free' } });
  // A line for your logs — and, marked `userland`, an event your service can keep for its users.
  logger?.info('plans defaulted', { userland: true, modifiedCount });
}

See Migration Logs for what the line carries and how to keep it.

When a migration runs inside a transaction, pass ctx.session to your driver calls so they participate in it:

ts
export const useTransaction = true;

export async function up({ db, session }: MigrationContext): Promise<void> {
  await db.collection('orders').updateMany({}, { $set: { migrated: true } }, { session });
}

Optional exports ​

ExportTypeEffect
up(ctx) => Promise<void>Required. Applies the migration.
down(ctx) => Promise<void>Required. Reverts the migration.
useTransactionbooleanWrap this file in a MongoDB transaction.
descriptionstringShown in the migronaut status table.

If up or down is missing or not a function, the loader throws MigrationInvalidExportError.

File naming ​

By default files are timestamped: 20260605143021-add-users-index.ts. Migrations run in ascending filename order, so the timestamp prefix guarantees deterministic ordering. Set sequential: true in config for 0001-, 0002- numbering instead.

Running TypeScript migrations ​

migronaut runs under your installed Node — it does not bundle a TypeScript loader. How a .ts migration loads depends on your Node version:

Your Node.ts migrationsSetup needed
≥ 22.18Load natively via built-in type strippingNone
< 22.18Need a TypeScript loader, or use .jsSee below

The simplest path

On Node ≥ 22.18, .ts migrations just work — nothing to install. If you're on older Node and don't want to set up a loader, author migrations as .js: they run everywhere with zero setup.

Using .ts on Node < 22.18 ​

Install tsx and run migronaut under it so its loader is registered before the migration is imported:

bash
npm install -D tsx
json
{
  "scripts": {
    "migrate": "node --import tsx node_modules/@alexify/migronaut/bin/migronaut.js up"
  }
}

tsx is your dev dependency here — migronaut doesn't ship it. The same applies when you call the library programmatically: run your script under Node ≥ 22.18 or with a TypeScript loader registered.

If a .ts file can't be loaded, migronaut throws a clear error explaining these options — see Troubleshooting.

Released under the MIT License.