$npx -y skills add get-convex/agent-skills --skill convex-migration-helperPlans Convex schema and data migrations with widen-migrate-narrow and @convex-dev/migrations. Use for breaking schema changes, backfills, table reshaping, or zero-downtime rollouts.
| 1 | # Convex Migration Helper |
| 2 | |
| 3 | Safely migrate Convex schemas and data when making breaking changes. |
| 4 | |
| 5 | ## When to Use |
| 6 | |
| 7 | - Adding new required fields to existing tables |
| 8 | - Changing field types or structure |
| 9 | - Splitting or merging tables |
| 10 | - Renaming or deleting fields |
| 11 | - Migrating from nested to relational data |
| 12 | |
| 13 | ## When Not to Use |
| 14 | |
| 15 | - Greenfield schema with no existing data in production or dev |
| 16 | - Adding optional fields that do not need backfilling |
| 17 | - Adding new tables with no existing data to migrate |
| 18 | - Adding or removing indexes with no correctness concern |
| 19 | - Questions about Convex schema design without a migration need |
| 20 | |
| 21 | ## Key Concepts |
| 22 | |
| 23 | ### Schema Validation Drives the Workflow |
| 24 | |
| 25 | Convex will not let you deploy a schema that does not match the data at rest. |
| 26 | This is the fundamental constraint that shapes every migration: |
| 27 | |
| 28 | - You cannot add a required field if existing documents don't have it |
| 29 | - You cannot change a field's type if existing documents have the old type |
| 30 | - You cannot remove a field from the schema if existing documents still have it |
| 31 | |
| 32 | This means migrations follow a predictable pattern: **widen the schema, migrate |
| 33 | the data, narrow the schema**. |
| 34 | |
| 35 | ### Online Migrations |
| 36 | |
| 37 | Convex migrations run online, meaning the app continues serving requests while |
| 38 | data is updated asynchronously in batches. During the migration window, your |
| 39 | code must handle both old and new data formats. |
| 40 | |
| 41 | ### Prefer New Fields Over Changing Types |
| 42 | |
| 43 | When changing the shape of data, create a new field rather than modifying an |
| 44 | existing one. This makes the transition safer and easier to roll back. |
| 45 | |
| 46 | ### Don't Delete Data |
| 47 | |
| 48 | Unless you are certain, prefer deprecating fields over deleting them. Mark the |
| 49 | field as `v.optional` and add a code comment explaining it is deprecated and why |
| 50 | it existed. |
| 51 | |
| 52 | ## Safe Changes (No Migration Needed) |
| 53 | |
| 54 | ### Adding Optional Field |
| 55 | |
| 56 | ```typescript |
| 57 | // Before |
| 58 | users: defineTable({ |
| 59 | name: v.string(), |
| 60 | }); |
| 61 | |
| 62 | // After - safe, new field is optional |
| 63 | users: defineTable({ |
| 64 | name: v.string(), |
| 65 | bio: v.optional(v.string()), |
| 66 | }); |
| 67 | ``` |
| 68 | |
| 69 | ### Adding New Table |
| 70 | |
| 71 | ```typescript |
| 72 | posts: defineTable({ |
| 73 | userId: v.id("users"), |
| 74 | title: v.string(), |
| 75 | }).index("by_user", ["userId"]); |
| 76 | ``` |
| 77 | |
| 78 | ### Adding Index |
| 79 | |
| 80 | ```typescript |
| 81 | users: defineTable({ |
| 82 | name: v.string(), |
| 83 | email: v.string(), |
| 84 | }).index("by_email", ["email"]); |
| 85 | ``` |
| 86 | |
| 87 | ## Breaking Changes: The Deployment Workflow |
| 88 | |
| 89 | Every breaking migration follows the same multi-deploy pattern: |
| 90 | |
| 91 | **Deploy 1 - Widen the schema:** |
| 92 | |
| 93 | 1. Update schema to allow both old and new formats (e.g., add optional new |
| 94 | field) |
| 95 | 2. Update code to handle both formats when reading |
| 96 | 3. Update code to write the new format for new documents |
| 97 | 4. Deploy |
| 98 | |
| 99 | **Between deploys - Migrate data:** |
| 100 | |
| 101 | 5. Run migration to backfill existing documents |
| 102 | 6. Verify all documents are migrated |
| 103 | |
| 104 | **Deploy 2 - Narrow the schema:** |
| 105 | |
| 106 | 7. Update schema to require the new format only |
| 107 | 8. Remove code that handles the old format |
| 108 | 9. Deploy |
| 109 | |
| 110 | ## Using the Migrations Component |
| 111 | |
| 112 | For any non-trivial migration, use the |
| 113 | [`@convex-dev/migrations`](https://www.convex.dev/components/migrations) |
| 114 | component. It handles batching, cursor-based pagination, state tracking, resume |
| 115 | from failure, dry runs, and progress monitoring. |
| 116 | |
| 117 | See `references/migrations-component.md` for installation, setup, defining and |
| 118 | running migrations directly with `npx convex run migrations:myMigration`, dry |
| 119 | runs, status monitoring, and configuration options. |
| 120 | |
| 121 | ## Common Migration Patterns |
| 122 | |
| 123 | See `references/migration-patterns.md` for complete patterns with code examples |
| 124 | covering: |
| 125 | |
| 126 | - Adding a required field |
| 127 | - Deleting a field |
| 128 | - Changing a field type |
| 129 | - Splitting nested data into a separate table |
| 130 | - Cleaning up orphaned documents |
| 131 | - Zero-downtime strategies (dual write, dual read) |
| 132 | - Small table shortcut (single internalMutation without the component) |
| 133 | - Verifying a migration is complete |
| 134 | |
| 135 | ## Common Pitfalls |
| 136 | |
| 137 | 1. **Making a field required before migrating data**: Convex rejects the deploy |
| 138 | because existing documents lack the field. Always widen the schema first. |
| 139 | 2. **Using `.collect()` on large tables**: Hits transaction limits or causes |
| 140 | timeouts. Use the migrations component for proper batched pagination. |
| 141 | `.collect()` is only safe for tables you know are small. |
| 142 | 3. **Not writing the new format before migrating**: Documents created during the |
| 143 | migration window will be missed, leaving unmigrated data after the migration |
| 144 | "completes." |
| 145 | 4. **Skipping the dry run**: Use `dryRun: true` to validate migration logic |
| 146 | before committing changes to production data. Catches bugs before they touch |
| 147 | real documents. |
| 148 | 5. **Deleting fields prematurely**: Prefer deprecating with `v.optional` and a |
| 149 | comment. Only delete after you are confident the data is no longer needed and |
| 150 | no code references it. |
| 151 | 6. **Using c |