5.0 KiB
Migrations Component Reference
Complete guide to the
@convex-dev/migrations
component for batched, resumable Convex data migrations.
Installation
npm install @convex-dev/migrations
Setup
// convex/convex.config.ts
import { defineApp } from "convex/server";
import migrations from "@convex-dev/migrations/convex.config.js";
const app = defineApp();
app.use(migrations);
export default app;
// convex/migrations.ts
import { Migrations } from "@convex-dev/migrations";
import { components } from "./_generated/api.js";
import { DataModel } from "./_generated/dataModel.js";
export const migrations = new Migrations<DataModel>(components.migrations);
The DataModel type parameter is optional but provides type safety for
migration definitions.
Define a Migration
The migrateOne function processes a single document. The component handles
batching and pagination automatically.
// convex/migrations.ts
export const addDefaultRole = migrations.define({
table: "users",
migrateOne: async (ctx, user) => {
if (user.role === undefined) {
await ctx.db.patch(user._id, { role: "user" });
}
},
});
Shorthand: if you return an object, it is applied as a patch automatically.
export const clearDeprecatedField = migrations.define({
table: "users",
migrateOne: () => ({ legacyField: undefined }),
});
Run a Migration
From the CLI:
npx convex run migrations:addDefaultRole
# Pass --prod to run in production.
npx convex run migrations:addDefaultRole --prod
The migration exported by migrations.define is directly callable from the CLI
or dashboard. You do not need a separate one-off runner for normal single
migrations.
If you want a general-purpose runner that accepts a migration name, define one:
export const run = migrations.runner();
Then call it with the full function name:
npx convex run migrations:run '{"fn": "migrations:addDefaultRole"}'
Programmatically from another Convex function:
await migrations.runOne(ctx, internal.migrations.addDefaultRole);
Run Multiple Migrations in Order
For a short ad hoc series, pass next when starting the first migration:
npx convex run migrations:addDefaultRole '{"next":["migrations:clearDeprecatedField","migrations:normalizeEmails"]}'
For a reusable series, define a runner:
export const runAll = migrations.runner([
internal.migrations.addDefaultRole,
internal.migrations.clearDeprecatedField,
internal.migrations.normalizeEmails,
]);
npx convex run migrations:runAll
If one fails, it stops and will not continue to the next. Call it again to retry from where it left off. Completed migrations are skipped automatically.
Programmatically from another Convex function:
await migrations.runSerially(ctx, [
internal.migrations.addDefaultRole,
internal.migrations.clearDeprecatedField,
internal.migrations.normalizeEmails,
]);
Dry Run
Test a migration before committing changes:
npx convex run migrations:addDefaultRole '{"dryRun": true}'
This runs one batch and then rolls back, so you can see what it would do without changing any data.
Restart a Migration
Pass reset: true to restart a migration from the beginning:
npx convex run migrations:addDefaultRole '{"reset": true}'
If you specify next or run a defined series, reset: true resets the cursor
for all migrations in the group.
Check Migration Status
npx convex run --component migrations lib:getStatus --watch
Cancel a Running Migration
npx convex run --component migrations lib:cancel '{"name": "migrations:addDefaultRole"}'
Or programmatically:
await migrations.cancel(ctx, internal.migrations.addDefaultRole);
Run Migrations on Deploy
Chain migration execution after deploying:
npx convex deploy --cmd 'npm run build' && npx convex run migrations:runAll --prod
Configuration Options
Custom Batch Size
If documents are large or the table has heavy write traffic, reduce the batch size to avoid transaction limits or OCC conflicts:
export const migrateHeavyTable = migrations.define({
table: "largeDocuments",
batchSize: 10,
migrateOne: async (ctx, doc) => {
// migration logic
},
});
Migrate a Subset Using an Index
Process only matching documents instead of the full table:
export const fixEmptyNames = migrations.define({
table: "users",
customRange: (query) => query.withIndex("by_name", (q) => q.eq("name", "")),
migrateOne: () => ({ name: "<unknown>" }),
});
Parallelize Within a Batch
By default each document in a batch is processed serially. Enable parallel processing if your migration logic does not depend on ordering:
export const clearField = migrations.define({
table: "myTable",
parallelize: true,
migrateOne: () => ({ optionalField: undefined }),
});