Your first migration
This tutorial walks you through the full lifecycle of a database migration with Mongoat: scaffold a migration file, write up and down, apply it, confirm it in the migration status, then revert and re-apply it. By the end you'll have run the whole cycle once, end to end.
Prerequisites
- Node.js
^20.19.0or>=22.12.0 - MongoDB running as a single-node replica set — this tutorial takes the transactional path, which every migration uses by default, and that requires a replica set (a standalone
mongodwon't work here) @iamcalegari/mongoatinstalled (npm install @iamcalegari/mongoat)tsxinstalled as a dev dependency — this tutorial writes a TypeScript migration:
npm install -D tsx- The connection env vars set:
MONGODB_URIandMONGODB_DB_NAME
1. Scaffold the migration
mongoat create backfill-user-statusCreated migrations/20260719120000_backfill-user-status.tsThe file that appears looks like this:
import { defineMigration } from '@iamcalegari/mongoat';
import type { MigrationContext } from '@iamcalegari/mongoat';
export const { up, down } = defineMigration({
async up(ctx: MigrationContext): Promise<void> {
// TODO: implement
},
async down(ctx: MigrationContext): Promise<void> {
// TODO: implement (optional — delete this to make the migration irreversible)
},
});2. Fill in up
Backfill a status field onto every users document that doesn't have one yet — a small, concrete change against a collection you can picture. Pass ctx.session so the update runs inside the migration's transaction:
export const { up, down } = defineMigration({
async up(ctx: MigrationContext): Promise<void> {
await ctx.db
.collection('users')
.updateMany(
{ status: { $exists: false } },
{ $set: { status: 'active' } },
{ session: ctx.session }
);
},
// ...
});3. Fill in down
The inverse: remove the field again.
export const { up, down } = defineMigration({
// ...
async down(ctx: MigrationContext): Promise<void> {
await ctx.db
.collection('users')
.updateMany({}, { $unset: { status: '' } }, { session: ctx.session });
},
});Writing down now, while the change is fresh in your head, is much cheaper than reconstructing it later under pressure — and it's what makes the revert you'll run in step 5 possible at all.
4. Apply it
mongoat upMigrations applied.5. Confirm with status
mongoat statusversion | name | applied
20260719120000 | backfill-user-status | applied
lock: free6. Undo it, then re-apply
mongoat down 20260719120000Reverted migration 20260719120000.Run status again and watch the row flip back to pending:
mongoat statusversion | name | applied
20260719120000 | backfill-user-status | pending
lock: freeRe-apply it to finish where you started:
mongoat upMigrations applied.You've now run the whole cycle: scaffold, apply, confirm, revert, and re-apply. For every flag, the failure modes, the four status labels, running migrations programmatically, and the ctx.schema helpers, see the how-to.
Next steps
- Write and run migrations — every flag, both env vars, the programmatic API, and how to recover when something goes wrong.
- Use transactions & sessions — more on sessions and
{ session }, which the migration runner uses under the hood. - Reference — the full public API generated from the source.