avoid-feature-creep
Keeps a change scoped to what was asked. Flags extra features, refactors, and 'while we are here' edits, and moves them to a…
Changes a live Convex schema without downtime: make a field optional, backfill in batches, flip the validator, then clean up. Uses the @convex-dev/migrations component where it fits. Use when renaming or removing a field, changing a type, splitting a table, or when deploy fails
$ npx -y skills add waynesutton/builder-skills --skill convex-migrations --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/convex-migrationsContext preview
The summary Claude sees to decide when to auto-load this skill.
Changes a live Convex schema without downtime: make a field optional, backfill in batches, flip the validator, then clean up. Uses the @convex-dev/migrations component where it fits. Use when renaming or removing a field, changing a type, splitting a table, or when deploy fails
name: convex-migrations description: Changes a live Convex schema without downtime: make a field optional, backfill in batches, flip the validator, then clean up. Uses the @convex-dev/migrations component where it fits. Use when renaming or removing a field, changing a type, splitting a table, or when deploy fails with a schema validation error on existing documents.
Produces a schema change plus a batched backfill that keeps every deploy green. The one rule: the schema must describe the documents that exist right now, not the documents you want. Change the data first, then tighten the validator.
Convex has no migration files or `migrate` command. `npx convex dev` pushes the schema and validates every existing document against it. Existing data is never transformed for you.
For step by step recipes (rename, change type, split, merge, add required, remove) open [references/migration-patterns.md](references/migration-patterns.md). For installing and running `@convex-dev/migrations` open [references/migrations-component.md](references/migrations-component.md).
Every migration follows the same six steps. Skipping one is how deploys break.
1. Make the field optional in `convex/schema.ts` (or add the new field as `v.optional`). 2. Deploy with `npx convex dev`. Update readers to handle `undefined` and writers to set the new shape. 3. Backfill existing documents in batches with an internal mutation or the migrations component. 4. Flip the validator to its final shape (`v.string()` instead of `v.optional(v.string())`, or drop the old field). 5. Deploy again. Validation now passes because every document already matches. 6. Clean up: delete fallback code, remove the backfill function, drop stale indexes.
Steps 1 and 2 must ship before step 3 starts. Otherwise new writes keep producing old shaped documents while the backfill runs.
One `internalMutation` that pages through the table with `paginate`, patches only documents that still need it, then reschedules itself with the continue cursor. Each batch is its own transaction, so a large table never hits the per function limit.
// convex/migrations.ts
import { internalMutation } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values";
const BATCH_SIZE = 100;
export const backfillAvatarUrl = internalMutation({
args: { cursor: v.union(v.string(), v.null()) },
returns: v.null(),
handler: async (ctx, args) => {
const result = await ctx.db
.query("users")
.paginate({ numItems: BATCH_SIZE, cursor: args.cursor });
for (const user of result.page) {
// Idempotent: skip documents that already have the field
if (user.avatarUrl === undefined) {
await ctx.db.patch(user._id, {
avatarUrl: defaultAvatar(user.name),
});
}
}
// Reschedule with the cursor until the table is exhausted
if (!result.isDone) {
await ctx.scheduler.runAfter(0, internal.migrations.backfillAvatarUrl, {
cursor: result.continueCursor,
});
}
return null;
},
});
function defaultAvatar(name: string): string {
return `https://api.dicebear.com/7.x/initials/svg?seed=${encodeURIComponent(name)}`;
}Start it from the CLI against the dev deployment:
npx convex run migrations:backfillAvatarUrl '{"cursor": null}'Keep the mutation idempotent. Re running it after a partial failure must be safe. Use `internalMutation`, never a public `mutation`, so it cannot be called from a client. Use `ctx.db.patch` for adding or changing fields and `ctx.db.patch(id, { field: undefined })` to remove one.
`@convex-dev/migrations` wraps the pattern above and adds what the hand rolled version lacks: persisted progress per migration, status checks, dry runs, cancel, and ordered runs of several migrations. Reach for it when:
Minimal 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";
import { DataModel } from "./_generated/dataModel";
export const migrations = new Migrations<DataModel>(components.migrations);
export const addDefaultRole = migrations.define({
table: "users",
migrateOne: async (ctx, user) => {
if (user.role === undefined) {
await ctx.db.patch(user._id, { role: "user" });
}
},
});npx convex run migrations:addDefaultRole '{"dryRun": true}'
npx convex run migrations:addDefaultRoleA one off backfill on a small table does not need the component. The hand rolled mutation is fine.
When a schema push fails, `npx convex dev` prints the table, one offending document id, and the field that does not match:
Schema validation failed Document with ID "j57abc..." in table "users" does not match the schema: Object is missing the required field `avatarUrl`. Consider wrapping the field validator in `v.optional(...)` if this is expected.
Read it as: existing data in `users` predates the field. The fix is never to delete the doc
Agent skills for builders shipping Convex apps. By Wayne Sutton. Seventeen skills.
Keeps a change scoped to what was asked. Flags extra features, refactors, and 'while we are here' edits, and moves them to a…
Builds AI agents on the Convex agent component: threads, messages, tools that call queries and mutations, streaming, RAG with…
Production patterns for Convex apps and the rules the @convex-dev/eslint-plugin enforces: validators, indexes, idempotent…
Creates reusable Convex components with defineComponent, a clean client wrapper, their own schema, and an npm publish setup. Use…
Schedules work in Convex: cron jobs in convex/crons.ts, one off scheduled functions with runAfter and runAt, batching large jobs,…
Handles files in Convex: upload URLs, storing blobs from actions, serving with getUrl or an HTTP action, metadata from the…