Files
finanzen/.agents/skills/convex-migration-helper/references/migrations-component.md
2026-06-15 11:33:23 +02:00

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 }),
});