
Migration Helper
Plan and run Convex schema and data migrations safely when adding required fields, ret typing tables, or reshaping relational data without taking the app down.
Overview
migration-helper is an agent skill most often used in Build (also Ship testing and Operate iterate) that plans and executes safe Convex schema and data migrations when changes affect existing documents.
Install
npx skills add https://github.com/get-convex/agent-skills --skill migration-helperWhat is this skill?
- Documents safe additive changes: optional fields, new tables, and new indexes without migration code
- Breaking-change playbook: required fields, type changes, renames, splits/merges, nested-to-relational moves
- Four migration principles including no automatic Convex migrations and zero-downtime-oriented patterns
- TypeScript schema examples for before/after tables and index definitions
- 4 stated migration principles
- 3 categories of safe changes without migration code
Adoption & trust: 523 installs on skills.sh; 31 GitHub stars; 3/3 security scanners passed (skills.sh audits).
What problem does it solve?
You changed Convex schema.ts but existing rows do not match new required fields or types, and Convex will not auto-migrate production data for you.
Who is it for?
Indie SaaS teams on Convex who need checklist-driven schema evolution with clear safe vs breaking change boundaries.
Skip if: Greenfield Convex projects with empty tables only, or teams on ORMs/frameworks outside Convex who need generic SQL migrations.
When should I use this skill?
Schema changes affect existing Convex data—new required fields, type or structure changes, table splits/merges, renames, or nested-to-relational moves.
What do I get? / Deliverables
You get a categorized migration plan—safe deploy steps versus scripted backfills—with zero-downtime guidance and TypeScript patterns ready to implement in Convex functions.
- Migration plan classifying safe vs breaking edits
- TypeScript schema before/after examples
- Implementation notes for zero-downtime backfill strategy
Recommended Skills
Journey fit
Spans multiple journey phases - primary shelf plus alternate fits below.
Schema and data shape changes are authored with backend code in build, even when rollout discipline spans ship. Backend is the canonical shelf for Convex defineTable changes, migration functions, and index additions tied to application data models.
Where it fits
Add a required profile field and sketch a backfill mutation before deploying schema.ts.
Validate migration batches and indexes on staging data copies before promoting to production.
Run follow-up transformation jobs after discovering legacy nested documents in production.
How it compares
Convex-specific migration playbook, not a general database flyway tool or automatic schema sync service.
Common Questions / FAQ
Who is migration-helper for?
Solo and small teams shipping Convex backends who touch schema.ts and must preserve live document data across deploys.
When should I use migration-helper?
During build/backend when adding required fields or restructuring tables; during ship/testing when validating migration batches; and during operate/iterate when follow-up data fixes are needed after a schema deploy.
Is migration-helper safe to install?
It is documentation and code-pattern guidance without declared agent tools; review the Security Audits panel on this Prism page for the parent get-convex/agent-skills package.
SKILL.md
READMESKILL.md - Migration Helper
# Convex Migration Helper Safely migrate Convex schemas and data when making breaking changes. ## When to Use - Adding new required fields to existing tables - Changing field types or structure - Splitting or merging tables - Renaming fields - Migrating from nested to relational data ## Migration Principles 1. **No Automatic Migrations**: Convex doesn't automatically migrate data 2. **Additive Changes are Safe**: Adding optional fields or new tables is safe 3. **Breaking Changes Need Code**: Required fields, type changes need migration code 4. **Zero-Downtime**: Write migrations to keep app running during migration ## Safe Changes (No Migration Needed) ### Adding Optional Field ```typescript // Before users: defineTable({ name: v.string(), }) // After - Safe! New field is optional users: defineTable({ name: v.string(), bio: v.optional(v.string()), }) ``` ### Adding New Table ```typescript // Safe to add completely new tables posts: defineTable({ userId: v.id("users"), title: v.string(), }).index("by_user", ["userId"]) ``` ### Adding Index ```typescript // Safe to add indexes at any time users: defineTable({ name: v.string(), email: v.string(), }) .index("by_email", ["email"]) // New index ``` ## Breaking Changes (Migration Required) ### Adding Required Field **Problem**: Existing documents won't have the new field. **Solution**: Add as optional first, backfill data, then make required. ```typescript // Step 1: Add as optional users: defineTable({ name: v.string(), email: v.optional(v.string()), // Start optional }) // Step 2: Create migration import { internalMutation } from "./_generated/server"; import { v } from "convex/values"; export const backfillEmails = internalMutation({ args: {}, handler: async (ctx) => { const users = await ctx.db.query("users").collect(); for (const user of users) { if (!user.email) { await ctx.db.patch(user._id, { email: `user-${user._id}@example.com`, // Default value }); } } }, }); // Step 3: Run migration via dashboard or CLI // npx convex run migrations:backfillEmails // Step 4: Make field required (after all data migrated) users: defineTable({ name: v.string(), email: v.string(), // Now required }) ``` ### Changing Field Type **Example**: Change `tags: v.array(v.string())` to separate table ```typescript // Step 1: Create new structure (additive) tags: defineTable({ name: v.string(), }).index("by_name", ["name"]), postTags: defineTable({ postId: v.id("posts"), tagId: v.id("tags"), }) .index("by_post", ["postId"]) .index("by_tag", ["tagId"]), // Keep old field as optional during migration posts: defineTable({ title: v.string(), tags: v.optional(v.array(v.string())), // Keep temporarily }) // Step 2: Write migration export const migrateTags = internalMutation({ args: { batchSize: v.optional(v.number()) }, handler: async (ctx, args) => { const batchSize = args.batchSize ?? 100; const posts = await ctx.db .query("posts") .filter(q => q.neq(q.field("tags"), undefined)) .take(batchSize); for (const post of posts) { if (!post.tags || post.tags.length === 0) { await ctx.db.patch(post._id, { tags: undefined }); continue; } // Create tags and relationships for (const tagName of post.tags) { // Get or create tag let tag = await ctx.db .query("tags") .withIndex("by_name", q => q.eq("name", tagName)) .unique(); if (!tag) { const tagId = await ctx.db.insert("tags", { name: tagName }); tag = { _id: tagId, name: tagName }; } // Create relationship const existing = await ctx.db .query("postTags")