Docs

Migrations

How to run Convex data migrations safely in production.

Convoy uses Convex for durable application data. Any migration that changes existing documents should follow a staged deployment flow: widen the schema, migrate and verify the data, then narrow the schema in a later deploy.

Default: run all pending migrations

The normal production path is to deploy the migration-capable version, dry-run all pending migrations, run all pending migrations, then verify the migration-specific invariants.

From apps/web:

npx convex run --prod migrations:runAll '{"dryRun": true}'
npx convex run --prod migrations:runAll
npx convex run --prod --component migrations lib:getStatus

If runAll dry-run reports Dry run attempted to update state while encountering already-completed migrations, check component status and dry-run the specific pending migration directly:

npx convex run --prod --component migrations lib:getStatus
npx convex run --prod migrations:<migrationName> '{"dryRun": true}'

That fallback is only for dry-run. The normal write path should still use migrations:runAll unless you intentionally need to run one migration in isolation.

Production flow

1. Deploy the widened version

The first deploy must be able to read both old and new data. It should:

  • keep the old schema shape valid
  • write the new shape for newly created or updated rows
  • include the migration function and verification queries
  • avoid removing legacy fields or union values until after verification

From apps/web:

npx convex deploy

If production is deployed by CI, push the widened commit and wait for the pipeline to deploy Convex functions and schema.

2. Dry-run pending migrations

Run all pending migrations against production with dryRun: true before writing data:

npx convex run --prod migrations:runAll '{"dryRun": true}'

Review the output for the number of rows processed and any sampled changes. If Convex says it cannot find the function, production has not deployed the widened function bundle yet.

If runAll dry-run encounters already-completed migrations and fails while trying to update dry-run state, use the fallback from the default flow above and dry-run the specific pending migration directly.

3. Run pending migrations

After the dry-run output looks correct, run all pending migrations:

npx convex run --prod migrations:runAll

For migrations registered with the migrations component, check status with:

npx convex run --prod --component migrations lib:getStatus

You can run a single migration directly when you are deliberately isolating one change:

npx convex run --prod migrations:<migrationName>

4. Verify the data

Every production migration should have at least one verification query. Run it after the migration finishes:

npx convex run --prod migrations:<verifyQueryName>

The verification should prove that no active rows still require the legacy schema shape. If it reports leftovers, fix the migration or the data and verify again before narrowing.

5. Deploy the narrowed version

Only after verification passes, ship a separate cleanup deploy that removes the old schema shape and transitional code. This is the point where you can remove legacy validators, deprecated fields, and read-time compatibility shims.

Migration log

Append production data migrations here after they are run and verified. Keep entries concise: ticket, date, migration function names, and the verification command names used after the run.

DateTicketMigrationVerification
YYYY-MM-DDTicketMigration function namesVerification function names

Example checklist

For a future migration:

  1. Deploy the widened code and schema.

  2. Dry-run production:

    npx convex run --prod migrations:<migrationName> '{"dryRun": true}'
  3. Run production:

    npx convex run --prod migrations:<migrationName>
  4. Verify production:

    npx convex run --prod migrations:<verifyQueryName>
  5. Deploy the narrowed code and schema only after verification passes.

Rollback notes

The widened deploy is usually rollback-friendly because it still accepts the old data. The narrowed deploy is different: once a schema removes a legacy field or union value, rolling back code may not be enough because the data has already changed.

Treat narrowing deploys as fix-forward by default. If rollback might be needed, take a Convex backup or export before narrowing and read Backups and rollback before deploying.

On this page