convex-migrations
Convexデータベースで、新しい項目の追加やデータ構造の変更、データの補完、項目の名前変更など、データベースの移行とスキーマの進化をスムーズに行うSkill。
📜 元の英語説明(参考)
Database migrations and schema evolution in Convex. Use when adding new fields, changing data structures, backfilling data, renaming fields, or performing zero-downtime schema changes.
🇯🇵 日本人クリエイター向け解説
Convexデータベースで、新しい項目の追加やデータ構造の変更、データの補完、項目の名前変更など、データベースの移行とスキーマの進化をスムーズに行うSkill。
※ jpskill.com 編集部が日本のビジネス現場向けに補足した解説です。Skill本体の挙動とは独立した参考情報です。
下記のコマンドをコピーしてターミナル(Mac/Linux)または PowerShell(Windows)に貼り付けてください。 ダウンロード → 解凍 → 配置まで全自動。
mkdir -p ~/.claude/skills && cd ~/.claude/skills && curl -L -o convex-migrations.zip https://jpskill.com/download/8732.zip && unzip -o convex-migrations.zip && rm convex-migrations.zip
$d = "$env:USERPROFILE\.claude\skills"; ni -Force -ItemType Directory $d | Out-Null; iwr https://jpskill.com/download/8732.zip -OutFile "$d\convex-migrations.zip"; Expand-Archive "$d\convex-migrations.zip" -DestinationPath $d -Force; ri "$d\convex-migrations.zip"
完了後、Claude Code を再起動 → 普通に「動画プロンプト作って」のように話しかけるだけで自動発動します。
💾 手動でダウンロードしたい(コマンドが難しい人向け)
- 1. 下の青いボタンを押して
convex-migrations.zipをダウンロード - 2. ZIPファイルをダブルクリックで解凍 →
convex-migrationsフォルダができる - 3. そのフォルダを
C:\Users\あなたの名前\.claude\skills\(Win)または~/.claude/skills/(Mac)へ移動 - 4. Claude Code を再起動
⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。
🎯 このSkillでできること
下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。
📦 インストール方法 (3ステップ)
- 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
- 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
- 3. 展開してできたフォルダを、ホームフォルダの
.claude/skills/に置く- · macOS / Linux:
~/.claude/skills/ - · Windows:
%USERPROFILE%\.claude\skills\
- · macOS / Linux:
Claude Code を再起動すれば完了。「このSkillを使って…」と話しかけなくても、関連する依頼で自動的に呼び出されます。
詳しい使い方ガイドを見る →- 最終更新
- 2026-05-18
- 取得日時
- 2026-05-18
- 同梱ファイル
- 1
📖 Skill本文(日本語訳)
※ 原文(英語/中国語)を Gemini で日本語化したものです。Claude 自身は原文を読みます。誤訳がある場合は原文をご確認ください。
Convex マイグレーション
マイグレーション戦略
Convex は「段階的なマイグレーション」アプローチを採用しています。
- スキーマに新しいオプションのフィールドを追加します。
- 古いフィールドと新しいフィールドの両方に書き込むコードをデプロイします。
- 既存のデータをバックフィルするマイグレーションを実行します。
- 新しいフィールドのみを使用するコードをデプロイします。
- スキーマから古いフィールドを削除します。
新しいフィールドの追加
ステップ 1: スキーマの更新 (オプションのフィールド)
// convex/schema.ts
export default defineSchema({
users: defineTable({
name: v.string(),
email: v.string(),
// 新しいフィールド - マイグレーション中はオプション
displayName: v.optional(v.string()),
}),
});
ステップ 2: 両方のフィールドへの書き込み
// convex/users.ts
export const create = mutation({
args: { name: v.string(), email: v.string() },
returns: v.id("users"),
handler: async (ctx, args) => {
return await ctx.db.insert("users", {
name: args.name,
email: args.email,
displayName: args.name, // 新しいフィールドへの書き込み
});
},
});
ステップ 3: バックフィル マイグレーション
// convex/migrations.ts
import { internalMutation } from "./_generated/server";
import { v } from "convex/values";
const BATCH_SIZE = 100;
export const backfillDisplayName = internalMutation({
args: { cursor: v.optional(v.string()) },
returns: v.union(v.string(), v.null()),
handler: async (ctx, args) => {
const result = await ctx.db
.query("users")
.paginate({ numItems: BATCH_SIZE, cursor: args.cursor ?? null });
for (const user of result.page) {
if (user.displayName === undefined) {
await ctx.db.patch(user._id, { displayName: user.name });
}
}
// 次のバッチのカーソルを返すか、完了した場合は null を返す
return result.isDone ? null : result.continueCursor;
},
});
ステップ 4: マイグレーションの実行
// convex/migrations.ts
export const runDisplayNameMigration = internalMutation({
args: {},
returns: v.null(),
handler: async (ctx) => {
let cursor: string | null = null;
do {
cursor = await ctx.runMutation(internal.migrations.backfillDisplayName, {
cursor: cursor ?? undefined,
});
} while (cursor !== null);
return null;
},
});
または、非同期処理にスケジューラを使用します。
export const startMigration = internalMutation({
args: {},
returns: v.null(),
handler: async (ctx) => {
await ctx.scheduler.runAfter(0, internal.migrations.backfillDisplayName, {});
return null;
},
});
export const backfillDisplayName = internalMutation({
args: { cursor: v.optional(v.string()) },
returns: v.null(),
handler: async (ctx, args) => {
const result = await ctx.db
.query("users")
.paginate({ numItems: BATCH_SIZE, cursor: args.cursor ?? null });
for (const user of result.page) {
if (user.displayName === undefined) {
await ctx.db.patch(user._id, { displayName: user.name });
}
}
// 次のバッチをスケジュールする
if (!result.isDone) {
await ctx.scheduler.runAfter(0, internal.migrations.backfillDisplayName, {
cursor: result.continueCursor,
});
}
return null;
},
});
フィールド名の変更
ステップ 1: 新しいフィールドの追加
// 両方のフィールドを持つスキーマ
users: defineTable({
userName: v.string(), // 古いフィールド
name: v.optional(v.string()), // 新しいフィールド
}),
ステップ 2: 両方に書き込み、新しい方から読み取る
export const update = mutation({
args: { userId: v.id("users"), name: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.patch(args.userId, {
userName: args.name, // 古いフィールド
name: args.name, // 新しいフィールド
});
return null;
},
});
export const get = query({
args: { userId: v.id("users") },
returns: v.union(v.object({ name: v.string() }), v.null()),
handler: async (ctx, args) => {
const user = await ctx.db.get(args.userId);
if (!user) return null;
// フォールバック付きで新しいフィールドから読み取る
return { name: user.name ?? user.userName };
},
});
ステップ 3: バックフィルと完了
マイグレーション後、新しいフィールドを必須にし、古いフィールドを削除するようにスキーマを更新します。
users: defineTable({
name: v.string(), // 必須になり、userName は削除されました
}),
フィールド型の変更
例: 文字列から配列へ
// 古い形式: tags: v.string() (カンマ区切り)
// 新しい形式: tags: v.array(v.string())
// ステップ 1: 新しいフィールドの追加
tags: v.optional(v.string()),
tagsArray: v.optional(v.array(v.string())),
// ステップ 2: マイグレーション
export const migrateTagsToArray = internalMutation({
args: { cursor: v.optional(v.string()) },
returns: v.null(),
handler: async (ctx, args) => {
const result = await ctx.db
.query("posts")
.paginate({ numItems: 100, cursor: args.cursor ?? null });
for (const post of result.page) {
if (post.tags && !post.tagsArray) {
const tagsArray = post.tags.split(",").map((t) => t.trim());
await ctx.db.patch(post._id, { tagsArray });
}
}
if (!result.isDone) {
await ctx.scheduler.runAfter(0, internal.migrations.migrateTagsToArray, {
cursor: result.continueCursor,
});
}
return null;
},
});
インデックスの変更
インデックスの追加
インデックスは、スキーマの変更をプッシュすると自動的に作成されます。
// スキーマに追加するだけ - マイグレーションは不要
users: defineTable({
email: v.string(),
createdAt: v.number(),
})
.index("by_email", ["email"])
.index("by_created", ["createdAt"]), // 新しいインデックス
インデックスの削除
スキーマから削除して再デプロイすると、古いインデックスは自動的に削除されます。
マイグレーションの追跡
マイグレーションの進捗状況を追跡します。
// convex/schema.ts
migrations: defineTable({
name: v.string(),
startedAt: v.number(),
completedAt: v.optional(v.number()),
processedCount: v.number(),
status: v.union(v.literal("running"), v.literal("completed"), v.literal("failed")),
error: v.optional(v.string()),
}).index("by_name", ["name"]),
// convex/migrations.ts
export const runMigration = internalMu 📜 原文 SKILL.md(Claudeが読む英語/中国語)を展開
Convex Migrations
Migration Strategy
Convex uses a "progressive migration" approach:
- Add new optional field to schema
- Deploy code that writes to both old and new fields
- Run migration to backfill existing data
- Deploy code that only uses new field
- Remove old field from schema
Adding a New Field
Step 1: Update Schema (Optional Field)
// convex/schema.ts
export default defineSchema({
users: defineTable({
name: v.string(),
email: v.string(),
// New field - optional during migration
displayName: v.optional(v.string()),
}),
});
Step 2: Write to Both Fields
// convex/users.ts
export const create = mutation({
args: { name: v.string(), email: v.string() },
returns: v.id("users"),
handler: async (ctx, args) => {
return await ctx.db.insert("users", {
name: args.name,
email: args.email,
displayName: args.name, // Write new field
});
},
});
Step 3: Backfill Migration
// convex/migrations.ts
import { internalMutation } from "./_generated/server";
import { v } from "convex/values";
const BATCH_SIZE = 100;
export const backfillDisplayName = internalMutation({
args: { cursor: v.optional(v.string()) },
returns: v.union(v.string(), v.null()),
handler: async (ctx, args) => {
const result = await ctx.db
.query("users")
.paginate({ numItems: BATCH_SIZE, cursor: args.cursor ?? null });
for (const user of result.page) {
if (user.displayName === undefined) {
await ctx.db.patch(user._id, { displayName: user.name });
}
}
// Return cursor for next batch, or null if done
return result.isDone ? null : result.continueCursor;
},
});
Step 4: Run Migration
// convex/migrations.ts
export const runDisplayNameMigration = internalMutation({
args: {},
returns: v.null(),
handler: async (ctx) => {
let cursor: string | null = null;
do {
cursor = await ctx.runMutation(internal.migrations.backfillDisplayName, {
cursor: cursor ?? undefined,
});
} while (cursor !== null);
return null;
},
});
Or use scheduler for async processing:
export const startMigration = internalMutation({
args: {},
returns: v.null(),
handler: async (ctx) => {
await ctx.scheduler.runAfter(0, internal.migrations.backfillDisplayName, {});
return null;
},
});
export const backfillDisplayName = internalMutation({
args: { cursor: v.optional(v.string()) },
returns: v.null(),
handler: async (ctx, args) => {
const result = await ctx.db
.query("users")
.paginate({ numItems: BATCH_SIZE, cursor: args.cursor ?? null });
for (const user of result.page) {
if (user.displayName === undefined) {
await ctx.db.patch(user._id, { displayName: user.name });
}
}
// Schedule next batch
if (!result.isDone) {
await ctx.scheduler.runAfter(0, internal.migrations.backfillDisplayName, {
cursor: result.continueCursor,
});
}
return null;
},
});
Renaming a Field
Step 1: Add New Field
// Schema with both fields
users: defineTable({
userName: v.string(), // Old field
name: v.optional(v.string()), // New field
}),
Step 2: Write to Both, Read from New
export const update = mutation({
args: { userId: v.id("users"), name: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.patch(args.userId, {
userName: args.name, // Old field
name: args.name, // New field
});
return null;
},
});
export const get = query({
args: { userId: v.id("users") },
returns: v.union(v.object({ name: v.string() }), v.null()),
handler: async (ctx, args) => {
const user = await ctx.db.get(args.userId);
if (!user) return null;
// Read from new field with fallback
return { name: user.name ?? user.userName };
},
});
Step 3: Backfill and Complete
After migration, update schema to require new field and remove old:
users: defineTable({
name: v.string(), // Now required, userName removed
}),
Changing Field Type
Example: String to Array
// Old: tags: v.string() (comma-separated)
// New: tags: v.array(v.string())
// Step 1: Add new field
tags: v.optional(v.string()),
tagsArray: v.optional(v.array(v.string())),
// Step 2: Migration
export const migrateTagsToArray = internalMutation({
args: { cursor: v.optional(v.string()) },
returns: v.null(),
handler: async (ctx, args) => {
const result = await ctx.db
.query("posts")
.paginate({ numItems: 100, cursor: args.cursor ?? null });
for (const post of result.page) {
if (post.tags && !post.tagsArray) {
const tagsArray = post.tags.split(",").map((t) => t.trim());
await ctx.db.patch(post._id, { tagsArray });
}
}
if (!result.isDone) {
await ctx.scheduler.runAfter(0, internal.migrations.migrateTagsToArray, {
cursor: result.continueCursor,
});
}
return null;
},
});
Index Changes
Adding an Index
Indexes are created automatically when you push schema changes:
// Just add to schema - no migration needed
users: defineTable({
email: v.string(),
createdAt: v.number(),
})
.index("by_email", ["email"])
.index("by_created", ["createdAt"]), // New index
Removing an Index
Remove from schema and redeploy - old index is dropped automatically.
Migration Tracking
Track migration progress:
// convex/schema.ts
migrations: defineTable({
name: v.string(),
startedAt: v.number(),
completedAt: v.optional(v.number()),
processedCount: v.number(),
status: v.union(v.literal("running"), v.literal("completed"), v.literal("failed")),
error: v.optional(v.string()),
}).index("by_name", ["name"]),
// convex/migrations.ts
export const runMigration = internalMutation({
args: { name: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
// Check if already running
const existing = await ctx.db
.query("migrations")
.withIndex("by_name", (q) => q.eq("name", args.name))
.unique();
if (existing?.status === "running") {
throw new ConvexError({ code: "CONFLICT", message: "Migration already running" });
}
// Create migration record
const migrationId = await ctx.db.insert("migrations", {
name: args.name,
startedAt: Date.now(),
processedCount: 0,
status: "running",
});
// Start migration
await ctx.scheduler.runAfter(0, internal.migrations.processBatch, {
migrationId,
cursor: undefined,
});
return null;
},
});
Common Patterns
Soft Delete Migration
// Add deletedAt field for soft deletes
export const migrateSoftDelete = internalMutation({
args: { cursor: v.optional(v.string()) },
returns: v.null(),
handler: async (ctx, args) => {
const result = await ctx.db
.query("items")
.paginate({ numItems: 100, cursor: args.cursor ?? null });
for (const item of result.page) {
if (item.deletedAt === undefined) {
await ctx.db.patch(item._id, { deletedAt: null });
}
}
if (!result.isDone) {
await ctx.scheduler.runAfter(0, internal.migrations.migrateSoftDelete, {
cursor: result.continueCursor,
});
}
return null;
},
});
Data Normalization
// Normalize email addresses to lowercase
export const normalizeEmails = internalMutation({
args: { cursor: v.optional(v.string()) },
returns: v.null(),
handler: async (ctx, args) => {
const result = await ctx.db
.query("users")
.paginate({ numItems: 100, cursor: args.cursor ?? null });
for (const user of result.page) {
const normalized = user.email.toLowerCase();
if (user.email !== normalized) {
await ctx.db.patch(user._id, { email: normalized });
}
}
if (!result.isDone) {
await ctx.scheduler.runAfter(0, internal.migrations.normalizeEmails, {
cursor: result.continueCursor,
});
}
return null;
},
});
Common Pitfalls
- Breaking changes - Always use optional fields during migration
- Large batches - Keep batch size under 100 to avoid timeouts
- Missing cursor - Always use pagination for large datasets
- No tracking - Log migration progress for debugging
- Skipping backfill - Don't assume code handles missing fields forever
References
- Schema: https://docs.convex.dev/database/schemas
- Best Practices: https://docs.convex.dev/understanding/best-practices/