Lifecycle Hooks
Hooks let you run code around the migration lifecycle — logging, metrics, Slack notifications, cache busting, and so on. Define them in your config under hooks.
Code-only option
Hooks are functions, so they can only live in a .ts/.js config — not in migronaut.config.json.
The five hooks
import type { MigrationHooks } from '@alexify/migronaut';
export default {
uri: 'mongodb://localhost:27017',
dbName: 'my_app',
hooks: {
/** Runs once before any migration in the batch starts */
beforeAll: async (ctx) => {
console.log('Starting migration batch…');
},
/** Runs once after the run ends — including when it failed */
afterAll: async (ctx, summary) => {
console.log(`Batch complete: ${summary.applied} applied (success: ${summary.success})`);
},
/** Runs before each individual migration */
beforeEach: async (name, ctx, info) => {
console.log(`→ ${name} (${info.index + 1}/${info.total})`);
},
/** Runs after each migration completes successfully */
afterEach: async (name, duration, ctx, info) => {
console.log(`✓ ${name} (${duration}ms, ${info.direction})`);
},
/** Runs when a migration throws — before the error propagates */
onError: async (name, error, ctx) => {
await notifySlack(`Migration ${name} failed: ${error.message}`);
},
} satisfies MigrationHooks,
};Signatures
| Hook | Signature | When it runs |
|---|---|---|
beforeAll | (ctx) => Promise<void> | Once, before the first migration in the run |
afterAll | (ctx, summary) => Promise<void> | Once, after the run ends — including when it failed |
beforeEach | (name, ctx, info) => Promise<void> | Before every individual migration (not for skipped ones) |
afterEach | (name, duration, ctx, info) => Promise<void> | After each migration succeeds |
onError | (name, error, ctx) => Promise<void> | When a migration throws, before it propagates |
ctx is the same MigrationContext passed to your migrations, so hooks have full database access. info is { direction, index, total }, so a hook can tell an apply from a revert and see its position in the run. summary is { success, applied, direction }.
A hook that throws fails the run with a HOOK_FAILED error — it is never swallowed, and never surfaces as an untyped Error.
Execution order
For a run of two migrations A then B:
beforeAll
beforeEach(A) → A.up() → afterEach(A)
beforeEach(B) → B.up() → afterEach(B)
afterAllIf A.up() throws:
beforeAll
beforeEach(A) → A.up() ✖ → onError(A) ← batch stops here, B never runs
afterAll({ success: false, applied: 0 })afterAll does run when the batch stops on an error — cleanup and notification hooks matter most on exactly that path. Check summary.success to tell the two cases apart.
Events
Hooks are configured up front and run inside the migration's flow. For metrics and alerting, subscribe to events instead: several listeners may attach from outside the config, and a listener that throws is contained rather than failing the run.
const kit = new MigratorKit(config);
kit.on('migration:success', ({ migration, durationMs, runId }) => {
metrics.timing('migration.duration', durationMs, { migration, runId });
});
kit.on('lock:lost', ({ reason }) => alert(`Migration lock lost: ${reason}`));
await kit.up();| Event | Payload |
|---|---|
run:start | { runId } |
run:end | { runId, success, error? } |
migration:start | { runId, migration, direction, batch? } |
migration:success | { runId, migration, direction, batch?, durationMs } |
migration:error | { runId, migration, direction, error } |
lock:acquired | { runId, owner } |
lock:released | { runId, owner } |
lock:lost | { runId, reason } |
runId is the same value on every event of a run, on the lock document, and on each changelog record that run writes — so logs, metrics and the database can be correlated after the fact.