vibesuite 1.3.3 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +75 -6
  2. package/assets/.agent/skills/avoid-feature-creep/SKILL.md +307 -0
  3. package/assets/.agent/skills/avoid-feature-creep/agents/openai.yaml +3 -0
  4. package/assets/.agent/skills/avoid-feature-creep/assets/large-logo.png +0 -0
  5. package/assets/.agent/skills/avoid-feature-creep/assets/small-logo.svg +17 -0
  6. package/assets/.agent/skills/convex/SKILL.md +62 -0
  7. package/assets/.agent/skills/convex/agents/openai.yaml +3 -0
  8. package/assets/.agent/skills/convex/assets/large-logo.png +0 -0
  9. package/assets/.agent/skills/convex/assets/small-logo.svg +17 -0
  10. package/assets/.agent/skills/convex-agents/SKILL.md +516 -0
  11. package/assets/.agent/skills/convex-agents/agents/openai.yaml +3 -0
  12. package/assets/.agent/skills/convex-agents/assets/large-logo.png +0 -0
  13. package/assets/.agent/skills/convex-agents/assets/small-logo.svg +17 -0
  14. package/assets/.agent/skills/convex-best-practices/SKILL.md +369 -0
  15. package/assets/.agent/skills/convex-best-practices/agents/openai.yaml +3 -0
  16. package/assets/.agent/skills/convex-best-practices/assets/large-logo.png +0 -0
  17. package/assets/.agent/skills/convex-best-practices/assets/small-logo.svg +17 -0
  18. package/assets/.agent/skills/convex-component-authoring/SKILL.md +457 -0
  19. package/assets/.agent/skills/convex-component-authoring/agents/openai.yaml +3 -0
  20. package/assets/.agent/skills/convex-component-authoring/assets/large-logo.png +0 -0
  21. package/assets/.agent/skills/convex-component-authoring/assets/small-logo.svg +17 -0
  22. package/assets/.agent/skills/convex-cron-jobs/SKILL.md +604 -0
  23. package/assets/.agent/skills/convex-cron-jobs/agents/openai.yaml +3 -0
  24. package/assets/.agent/skills/convex-cron-jobs/assets/large-logo.png +0 -0
  25. package/assets/.agent/skills/convex-cron-jobs/assets/small-logo.svg +17 -0
  26. package/assets/.agent/skills/convex-file-storage/SKILL.md +467 -0
  27. package/assets/.agent/skills/convex-file-storage/agents/openai.yaml +3 -0
  28. package/assets/.agent/skills/convex-file-storage/assets/large-logo.png +0 -0
  29. package/assets/.agent/skills/convex-file-storage/assets/small-logo.svg +17 -0
  30. package/assets/.agent/skills/convex-functions/SKILL.md +458 -0
  31. package/assets/.agent/skills/convex-functions/agents/openai.yaml +3 -0
  32. package/assets/.agent/skills/convex-functions/assets/large-logo.png +0 -0
  33. package/assets/.agent/skills/convex-functions/assets/small-logo.svg +17 -0
  34. package/assets/.agent/skills/convex-http-actions/SKILL.md +733 -0
  35. package/assets/.agent/skills/convex-http-actions/agents/openai.yaml +3 -0
  36. package/assets/.agent/skills/convex-http-actions/assets/large-logo.png +0 -0
  37. package/assets/.agent/skills/convex-http-actions/assets/small-logo.svg +17 -0
  38. package/assets/.agent/skills/convex-migrations/SKILL.md +712 -0
  39. package/assets/.agent/skills/convex-migrations/agents/openai.yaml +3 -0
  40. package/assets/.agent/skills/convex-migrations/assets/large-logo.png +0 -0
  41. package/assets/.agent/skills/convex-migrations/assets/small-logo.svg +17 -0
  42. package/assets/.agent/skills/convex-realtime/SKILL.md +443 -0
  43. package/assets/.agent/skills/convex-realtime/agents/openai.yaml +3 -0
  44. package/assets/.agent/skills/convex-realtime/assets/large-logo.png +0 -0
  45. package/assets/.agent/skills/convex-realtime/assets/small-logo.svg +17 -0
  46. package/assets/.agent/skills/convex-schema-validator/SKILL.md +400 -0
  47. package/assets/.agent/skills/convex-schema-validator/agents/openai.yaml +3 -0
  48. package/assets/.agent/skills/convex-schema-validator/assets/large-logo.png +0 -0
  49. package/assets/.agent/skills/convex-schema-validator/assets/small-logo.svg +17 -0
  50. package/assets/.agent/skills/convex-security-audit/SKILL.md +539 -0
  51. package/assets/.agent/skills/convex-security-audit/agents/openai.yaml +3 -0
  52. package/assets/.agent/skills/convex-security-audit/assets/large-logo.png +0 -0
  53. package/assets/.agent/skills/convex-security-audit/assets/small-logo.svg +17 -0
  54. package/assets/.agent/skills/convex-security-check/SKILL.md +378 -0
  55. package/assets/.agent/skills/convex-security-check/agents/openai.yaml +3 -0
  56. package/assets/.agent/skills/convex-security-check/assets/large-logo.png +0 -0
  57. package/assets/.agent/skills/convex-security-check/assets/small-logo.svg +17 -0
  58. package/assets/.agent/skills/github-ops/SKILL.md +4 -4
  59. package/assets/.agent/skills/google-trends/SKILL.md +7 -7
  60. package/assets/.agent/skills/optimize-agent-context/SKILL.md +97 -0
  61. package/assets/.agent/skills/youtube-pipeline/SKILL.md +10 -10
  62. package/assets/.agent/workflows/LEGACY/init_smart_ops.md +2 -2
  63. package/assets/.agent/workflows/agent_reset.md +4 -6
  64. package/assets/.agent/workflows/mode-orchestrator.md +17 -22
  65. package/assets/.agent/workflows/mode-visionary.md +3 -10
  66. package/assets/.agent/workflows/optimize-agent-context.md +54 -0
  67. package/assets/.agent/workflows/remotion-build.md +17 -17
  68. package/assets/.agent/workflows/stitch.md +4 -4
  69. package/assets/VibeCode-Agents/vibe-orchestrator.yaml +14 -33
  70. package/assets/VibeCode-Agents/vibe-visionary.yaml +3 -13
  71. package/package.json +1 -1
  72. package/src/cli.js +416 -20
  73. package/src/harness.js +281 -0
  74. package/src/store.js +239 -0
  75. package/assets/VibeCode-Agents/custom_modes.yaml +0 -979
@@ -0,0 +1,712 @@
1
+ ---
2
+ name: convex-migrations
3
+ displayName: Convex Migrations
4
+ description: Schema migration strategies for evolving applications including adding new fields, backfilling data, removing deprecated fields, index migrations, and zero-downtime migration patterns
5
+ version: 1.0.0
6
+ author: Convex
7
+ tags: [convex, migrations, schema, database, data-modeling]
8
+ ---
9
+
10
+ # Convex Migrations
11
+
12
+ Evolve your Convex database schema safely with patterns for adding fields, backfilling data, removing deprecated fields, and maintaining zero-downtime deployments.
13
+
14
+ ## Documentation Sources
15
+
16
+ Before implementing, do not assume; fetch the latest documentation:
17
+
18
+ - Primary: https://docs.convex.dev/database/schemas
19
+ - Schema Overview: https://docs.convex.dev/database
20
+ - Migration Patterns: https://stack.convex.dev/migrate-data-postgres-to-convex
21
+ - For broader context: https://docs.convex.dev/llms.txt
22
+
23
+ ## Instructions
24
+
25
+ ### Migration Philosophy
26
+
27
+ Convex handles schema evolution differently than traditional databases:
28
+
29
+ - No explicit migration files or commands
30
+ - Schema changes deploy instantly with `npx convex dev`
31
+ - Existing data is not automatically transformed
32
+ - Use optional fields and backfill mutations for safe migrations
33
+
34
+ ### Adding New Fields
35
+
36
+ Start with optional fields, then backfill:
37
+
38
+ ```typescript
39
+ // Step 1: Add optional field to schema
40
+ // convex/schema.ts
41
+ import { defineSchema, defineTable } from "convex/server";
42
+ import { v } from "convex/values";
43
+
44
+ export default defineSchema({
45
+ users: defineTable({
46
+ name: v.string(),
47
+ email: v.string(),
48
+ // New field - start as optional
49
+ avatarUrl: v.optional(v.string()),
50
+ }),
51
+ });
52
+ ```
53
+
54
+ ```typescript
55
+ // Step 2: Update code to handle both cases
56
+ // convex/users.ts
57
+ import { query } from "./_generated/server";
58
+ import { v } from "convex/values";
59
+
60
+ export const getUser = query({
61
+ args: { userId: v.id("users") },
62
+ returns: v.union(
63
+ v.object({
64
+ _id: v.id("users"),
65
+ name: v.string(),
66
+ email: v.string(),
67
+ avatarUrl: v.union(v.string(), v.null()),
68
+ }),
69
+ v.null()
70
+ ),
71
+ handler: async (ctx, args) => {
72
+ const user = await ctx.db.get(args.userId);
73
+ if (!user) return null;
74
+
75
+ return {
76
+ _id: user._id,
77
+ name: user.name,
78
+ email: user.email,
79
+ // Handle missing field gracefully
80
+ avatarUrl: user.avatarUrl ?? null,
81
+ };
82
+ },
83
+ });
84
+ ```
85
+
86
+ ```typescript
87
+ // Step 3: Backfill existing documents
88
+ // convex/migrations.ts
89
+ import { internalMutation } from "./_generated/server";
90
+ import { internal } from "./_generated/api";
91
+ import { v } from "convex/values";
92
+
93
+ const BATCH_SIZE = 100;
94
+
95
+ export const backfillAvatarUrl = internalMutation({
96
+ args: {
97
+ cursor: v.optional(v.string()),
98
+ },
99
+ returns: v.object({
100
+ processed: v.number(),
101
+ hasMore: v.boolean(),
102
+ }),
103
+ handler: async (ctx, args) => {
104
+ const result = await ctx.db
105
+ .query("users")
106
+ .paginate({ numItems: BATCH_SIZE, cursor: args.cursor ?? null });
107
+
108
+ let processed = 0;
109
+ for (const user of result.page) {
110
+ // Only update if field is missing
111
+ if (user.avatarUrl === undefined) {
112
+ await ctx.db.patch(user._id, {
113
+ avatarUrl: generateDefaultAvatar(user.name),
114
+ });
115
+ processed++;
116
+ }
117
+ }
118
+
119
+ // Schedule next batch if needed
120
+ if (!result.isDone) {
121
+ await ctx.scheduler.runAfter(0, internal.migrations.backfillAvatarUrl, {
122
+ cursor: result.continueCursor,
123
+ });
124
+ }
125
+
126
+ return {
127
+ processed,
128
+ hasMore: !result.isDone,
129
+ };
130
+ },
131
+ });
132
+
133
+ function generateDefaultAvatar(name: string): string {
134
+ return `https://api.dicebear.com/7.x/initials/svg?seed=${encodeURIComponent(name)}`;
135
+ }
136
+ ```
137
+
138
+ ```typescript
139
+ // Step 4: After backfill completes, make field required
140
+ // convex/schema.ts
141
+ export default defineSchema({
142
+ users: defineTable({
143
+ name: v.string(),
144
+ email: v.string(),
145
+ avatarUrl: v.string(), // Now required
146
+ }),
147
+ });
148
+ ```
149
+
150
+ ### Removing Fields
151
+
152
+ Remove field usage before removing from schema:
153
+
154
+ ```typescript
155
+ // Step 1: Stop using the field in queries and mutations
156
+ // Mark as deprecated in code comments
157
+
158
+ // Step 2: Remove field from schema (make optional first if needed)
159
+ // convex/schema.ts
160
+ export default defineSchema({
161
+ posts: defineTable({
162
+ title: v.string(),
163
+ content: v.string(),
164
+ authorId: v.id("users"),
165
+ // legacyField: v.optional(v.string()), // Remove this line
166
+ }),
167
+ });
168
+
169
+ // Step 3: Optionally clean up existing data
170
+ // convex/migrations.ts
171
+ export const removeDeprecatedField = internalMutation({
172
+ args: {
173
+ cursor: v.optional(v.string()),
174
+ },
175
+ returns: v.null(),
176
+ handler: async (ctx, args) => {
177
+ const result = await ctx.db
178
+ .query("posts")
179
+ .paginate({ numItems: 100, cursor: args.cursor ?? null });
180
+
181
+ for (const post of result.page) {
182
+ // Use replace to remove the field entirely
183
+ const { legacyField, ...rest } = post as typeof post & { legacyField?: string };
184
+ if (legacyField !== undefined) {
185
+ await ctx.db.replace(post._id, rest);
186
+ }
187
+ }
188
+
189
+ if (!result.isDone) {
190
+ await ctx.scheduler.runAfter(0, internal.migrations.removeDeprecatedField, {
191
+ cursor: result.continueCursor,
192
+ });
193
+ }
194
+
195
+ return null;
196
+ },
197
+ });
198
+ ```
199
+
200
+ ### Renaming Fields
201
+
202
+ Renaming requires copying data to new field, then removing old:
203
+
204
+ ```typescript
205
+ // Step 1: Add new field as optional
206
+ // convex/schema.ts
207
+ export default defineSchema({
208
+ users: defineTable({
209
+ userName: v.string(), // Old field
210
+ displayName: v.optional(v.string()), // New field
211
+ }),
212
+ });
213
+
214
+ // Step 2: Update code to read from new field with fallback
215
+ export const getUser = query({
216
+ args: { userId: v.id("users") },
217
+ returns: v.object({
218
+ _id: v.id("users"),
219
+ displayName: v.string(),
220
+ }),
221
+ handler: async (ctx, args) => {
222
+ const user = await ctx.db.get(args.userId);
223
+ if (!user) throw new Error("User not found");
224
+
225
+ return {
226
+ _id: user._id,
227
+ // Read new field, fall back to old
228
+ displayName: user.displayName ?? user.userName,
229
+ };
230
+ },
231
+ });
232
+
233
+ // Step 3: Backfill to copy data
234
+ export const backfillDisplayName = internalMutation({
235
+ args: { cursor: v.optional(v.string()) },
236
+ returns: v.null(),
237
+ handler: async (ctx, args) => {
238
+ const result = await ctx.db
239
+ .query("users")
240
+ .paginate({ numItems: 100, cursor: args.cursor ?? null });
241
+
242
+ for (const user of result.page) {
243
+ if (user.displayName === undefined) {
244
+ await ctx.db.patch(user._id, {
245
+ displayName: user.userName,
246
+ });
247
+ }
248
+ }
249
+
250
+ if (!result.isDone) {
251
+ await ctx.scheduler.runAfter(0, internal.migrations.backfillDisplayName, {
252
+ cursor: result.continueCursor,
253
+ });
254
+ }
255
+
256
+ return null;
257
+ },
258
+ });
259
+
260
+ // Step 4: After backfill, update schema to make new field required
261
+ // and remove old field
262
+ export default defineSchema({
263
+ users: defineTable({
264
+ // userName removed
265
+ displayName: v.string(),
266
+ }),
267
+ });
268
+ ```
269
+
270
+ ### Adding Indexes
271
+
272
+ Add indexes before using them in queries:
273
+
274
+ ```typescript
275
+ // Step 1: Add index to schema
276
+ // convex/schema.ts
277
+ export default defineSchema({
278
+ posts: defineTable({
279
+ title: v.string(),
280
+ authorId: v.id("users"),
281
+ publishedAt: v.optional(v.number()),
282
+ status: v.string(),
283
+ })
284
+ .index("by_author", ["authorId"])
285
+ // New index
286
+ .index("by_status_and_published", ["status", "publishedAt"]),
287
+ });
288
+
289
+ // Step 2: Deploy schema change
290
+ // Run: npx convex dev
291
+
292
+ // Step 3: Now use the index in queries
293
+ export const getPublishedPosts = query({
294
+ args: {},
295
+ returns: v.array(v.object({
296
+ _id: v.id("posts"),
297
+ title: v.string(),
298
+ publishedAt: v.number(),
299
+ })),
300
+ handler: async (ctx) => {
301
+ const posts = await ctx.db
302
+ .query("posts")
303
+ .withIndex("by_status_and_published", (q) =>
304
+ q.eq("status", "published")
305
+ )
306
+ .order("desc")
307
+ .take(10);
308
+
309
+ return posts
310
+ .filter((p) => p.publishedAt !== undefined)
311
+ .map((p) => ({
312
+ _id: p._id,
313
+ title: p.title,
314
+ publishedAt: p.publishedAt!,
315
+ }));
316
+ },
317
+ });
318
+ ```
319
+
320
+ ### Changing Field Types
321
+
322
+ Type changes require careful migration:
323
+
324
+ ```typescript
325
+ // Example: Change from string to number for a "priority" field
326
+
327
+ // Step 1: Add new field with new type
328
+ // convex/schema.ts
329
+ export default defineSchema({
330
+ tasks: defineTable({
331
+ title: v.string(),
332
+ priority: v.string(), // Old: "low", "medium", "high"
333
+ priorityLevel: v.optional(v.number()), // New: 1, 2, 3
334
+ }),
335
+ });
336
+
337
+ // Step 2: Backfill with type conversion
338
+ export const migratePriorityToNumber = internalMutation({
339
+ args: { cursor: v.optional(v.string()) },
340
+ returns: v.null(),
341
+ handler: async (ctx, args) => {
342
+ const result = await ctx.db
343
+ .query("tasks")
344
+ .paginate({ numItems: 100, cursor: args.cursor ?? null });
345
+
346
+ const priorityMap: Record<string, number> = {
347
+ low: 1,
348
+ medium: 2,
349
+ high: 3,
350
+ };
351
+
352
+ for (const task of result.page) {
353
+ if (task.priorityLevel === undefined) {
354
+ await ctx.db.patch(task._id, {
355
+ priorityLevel: priorityMap[task.priority] ?? 1,
356
+ });
357
+ }
358
+ }
359
+
360
+ if (!result.isDone) {
361
+ await ctx.scheduler.runAfter(0, internal.migrations.migratePriorityToNumber, {
362
+ cursor: result.continueCursor,
363
+ });
364
+ }
365
+
366
+ return null;
367
+ },
368
+ });
369
+
370
+ // Step 3: Update code to use new field
371
+ export const getTask = query({
372
+ args: { taskId: v.id("tasks") },
373
+ returns: v.object({
374
+ _id: v.id("tasks"),
375
+ title: v.string(),
376
+ priorityLevel: v.number(),
377
+ }),
378
+ handler: async (ctx, args) => {
379
+ const task = await ctx.db.get(args.taskId);
380
+ if (!task) throw new Error("Task not found");
381
+
382
+ const priorityMap: Record<string, number> = {
383
+ low: 1,
384
+ medium: 2,
385
+ high: 3,
386
+ };
387
+
388
+ return {
389
+ _id: task._id,
390
+ title: task.title,
391
+ priorityLevel: task.priorityLevel ?? priorityMap[task.priority] ?? 1,
392
+ };
393
+ },
394
+ });
395
+
396
+ // Step 4: After backfill, update schema
397
+ export default defineSchema({
398
+ tasks: defineTable({
399
+ title: v.string(),
400
+ // priority field removed
401
+ priorityLevel: v.number(),
402
+ }),
403
+ });
404
+ ```
405
+
406
+ ### Migration Runner Pattern
407
+
408
+ Create a reusable migration system:
409
+
410
+ ```typescript
411
+ // convex/schema.ts
412
+ import { defineSchema, defineTable } from "convex/server";
413
+ import { v } from "convex/values";
414
+
415
+ export default defineSchema({
416
+ migrations: defineTable({
417
+ name: v.string(),
418
+ startedAt: v.number(),
419
+ completedAt: v.optional(v.number()),
420
+ status: v.union(
421
+ v.literal("running"),
422
+ v.literal("completed"),
423
+ v.literal("failed")
424
+ ),
425
+ error: v.optional(v.string()),
426
+ processed: v.number(),
427
+ }).index("by_name", ["name"]),
428
+
429
+ // Your other tables...
430
+ });
431
+ ```
432
+
433
+ ```typescript
434
+ // convex/migrations.ts
435
+ import { internalMutation, internalQuery } from "./_generated/server";
436
+ import { internal } from "./_generated/api";
437
+ import { v } from "convex/values";
438
+
439
+ // Check if migration has run
440
+ export const hasMigrationRun = internalQuery({
441
+ args: { name: v.string() },
442
+ returns: v.boolean(),
443
+ handler: async (ctx, args) => {
444
+ const migration = await ctx.db
445
+ .query("migrations")
446
+ .withIndex("by_name", (q) => q.eq("name", args.name))
447
+ .first();
448
+ return migration?.status === "completed";
449
+ },
450
+ });
451
+
452
+ // Start a migration
453
+ export const startMigration = internalMutation({
454
+ args: { name: v.string() },
455
+ returns: v.id("migrations"),
456
+ handler: async (ctx, args) => {
457
+ // Check if already exists
458
+ const existing = await ctx.db
459
+ .query("migrations")
460
+ .withIndex("by_name", (q) => q.eq("name", args.name))
461
+ .first();
462
+
463
+ if (existing) {
464
+ if (existing.status === "completed") {
465
+ throw new Error(`Migration ${args.name} already completed`);
466
+ }
467
+ if (existing.status === "running") {
468
+ throw new Error(`Migration ${args.name} already running`);
469
+ }
470
+ // Reset failed migration
471
+ await ctx.db.patch(existing._id, {
472
+ status: "running",
473
+ startedAt: Date.now(),
474
+ error: undefined,
475
+ processed: 0,
476
+ });
477
+ return existing._id;
478
+ }
479
+
480
+ return await ctx.db.insert("migrations", {
481
+ name: args.name,
482
+ startedAt: Date.now(),
483
+ status: "running",
484
+ processed: 0,
485
+ });
486
+ },
487
+ });
488
+
489
+ // Update migration progress
490
+ export const updateMigrationProgress = internalMutation({
491
+ args: {
492
+ migrationId: v.id("migrations"),
493
+ processed: v.number(),
494
+ },
495
+ returns: v.null(),
496
+ handler: async (ctx, args) => {
497
+ const migration = await ctx.db.get(args.migrationId);
498
+ if (!migration) return null;
499
+
500
+ await ctx.db.patch(args.migrationId, {
501
+ processed: migration.processed + args.processed,
502
+ });
503
+
504
+ return null;
505
+ },
506
+ });
507
+
508
+ // Complete a migration
509
+ export const completeMigration = internalMutation({
510
+ args: { migrationId: v.id("migrations") },
511
+ returns: v.null(),
512
+ handler: async (ctx, args) => {
513
+ await ctx.db.patch(args.migrationId, {
514
+ status: "completed",
515
+ completedAt: Date.now(),
516
+ });
517
+ return null;
518
+ },
519
+ });
520
+
521
+ // Fail a migration
522
+ export const failMigration = internalMutation({
523
+ args: {
524
+ migrationId: v.id("migrations"),
525
+ error: v.string(),
526
+ },
527
+ returns: v.null(),
528
+ handler: async (ctx, args) => {
529
+ await ctx.db.patch(args.migrationId, {
530
+ status: "failed",
531
+ error: args.error,
532
+ });
533
+ return null;
534
+ },
535
+ });
536
+ ```
537
+
538
+ ```typescript
539
+ // convex/migrations/addUserTimestamps.ts
540
+ import { internalMutation } from "../_generated/server";
541
+ import { internal } from "../_generated/api";
542
+ import { v } from "convex/values";
543
+
544
+ const MIGRATION_NAME = "add_user_timestamps_v1";
545
+ const BATCH_SIZE = 100;
546
+
547
+ export const run = internalMutation({
548
+ args: {
549
+ migrationId: v.optional(v.id("migrations")),
550
+ cursor: v.optional(v.string()),
551
+ },
552
+ returns: v.null(),
553
+ handler: async (ctx, args) => {
554
+ // Initialize migration on first run
555
+ let migrationId = args.migrationId;
556
+ if (!migrationId) {
557
+ const hasRun = await ctx.runQuery(internal.migrations.hasMigrationRun, {
558
+ name: MIGRATION_NAME,
559
+ });
560
+ if (hasRun) {
561
+ console.log(`Migration ${MIGRATION_NAME} already completed`);
562
+ return null;
563
+ }
564
+ migrationId = await ctx.runMutation(internal.migrations.startMigration, {
565
+ name: MIGRATION_NAME,
566
+ });
567
+ }
568
+
569
+ try {
570
+ const result = await ctx.db
571
+ .query("users")
572
+ .paginate({ numItems: BATCH_SIZE, cursor: args.cursor ?? null });
573
+
574
+ let processed = 0;
575
+ for (const user of result.page) {
576
+ if (user.createdAt === undefined) {
577
+ await ctx.db.patch(user._id, {
578
+ createdAt: user._creationTime,
579
+ updatedAt: user._creationTime,
580
+ });
581
+ processed++;
582
+ }
583
+ }
584
+
585
+ // Update progress
586
+ await ctx.runMutation(internal.migrations.updateMigrationProgress, {
587
+ migrationId,
588
+ processed,
589
+ });
590
+
591
+ // Continue or complete
592
+ if (!result.isDone) {
593
+ await ctx.scheduler.runAfter(0, internal.migrations.addUserTimestamps.run, {
594
+ migrationId,
595
+ cursor: result.continueCursor,
596
+ });
597
+ } else {
598
+ await ctx.runMutation(internal.migrations.completeMigration, {
599
+ migrationId,
600
+ });
601
+ console.log(`Migration ${MIGRATION_NAME} completed`);
602
+ }
603
+ } catch (error) {
604
+ await ctx.runMutation(internal.migrations.failMigration, {
605
+ migrationId,
606
+ error: String(error),
607
+ });
608
+ throw error;
609
+ }
610
+
611
+ return null;
612
+ },
613
+ });
614
+ ```
615
+
616
+ ## Examples
617
+
618
+ ### Schema with Migration Support
619
+
620
+ ```typescript
621
+ // convex/schema.ts
622
+ import { defineSchema, defineTable } from "convex/server";
623
+ import { v } from "convex/values";
624
+
625
+ export default defineSchema({
626
+ // Migration tracking
627
+ migrations: defineTable({
628
+ name: v.string(),
629
+ startedAt: v.number(),
630
+ completedAt: v.optional(v.number()),
631
+ status: v.union(
632
+ v.literal("running"),
633
+ v.literal("completed"),
634
+ v.literal("failed")
635
+ ),
636
+ error: v.optional(v.string()),
637
+ processed: v.number(),
638
+ }).index("by_name", ["name"]),
639
+
640
+ // Users table with evolved schema
641
+ users: defineTable({
642
+ // Original fields
643
+ name: v.string(),
644
+ email: v.string(),
645
+
646
+ // Added in migration v1
647
+ createdAt: v.optional(v.number()),
648
+ updatedAt: v.optional(v.number()),
649
+
650
+ // Added in migration v2
651
+ avatarUrl: v.optional(v.string()),
652
+
653
+ // Added in migration v3
654
+ settings: v.optional(v.object({
655
+ theme: v.string(),
656
+ notifications: v.boolean(),
657
+ })),
658
+ })
659
+ .index("by_email", ["email"])
660
+ .index("by_createdAt", ["createdAt"]),
661
+
662
+ // Posts table with indexes for common queries
663
+ posts: defineTable({
664
+ title: v.string(),
665
+ content: v.string(),
666
+ authorId: v.id("users"),
667
+ status: v.union(
668
+ v.literal("draft"),
669
+ v.literal("published"),
670
+ v.literal("archived")
671
+ ),
672
+ publishedAt: v.optional(v.number()),
673
+ createdAt: v.number(),
674
+ updatedAt: v.number(),
675
+ })
676
+ .index("by_author", ["authorId"])
677
+ .index("by_status", ["status"])
678
+ .index("by_author_and_status", ["authorId", "status"])
679
+ .index("by_publishedAt", ["publishedAt"]),
680
+ });
681
+ ```
682
+
683
+ ## Best Practices
684
+
685
+ - Never run `npx convex deploy` unless explicitly instructed
686
+ - Never run any git commands unless explicitly instructed
687
+ - Always start with optional fields when adding new data
688
+ - Backfill data in batches to avoid timeouts
689
+ - Test migrations on development before production
690
+ - Keep track of completed migrations to avoid re-running
691
+ - Update code to handle both old and new data during transition
692
+ - Remove deprecated fields only after all code stops using them
693
+ - Use pagination for large datasets
694
+ - Add appropriate indexes before running queries on new fields
695
+
696
+ ## Common Pitfalls
697
+
698
+ 1. **Making new fields required immediately** - Breaks existing documents
699
+ 2. **Not handling undefined values** - Causes runtime errors
700
+ 3. **Large batch sizes** - Causes function timeouts
701
+ 4. **Forgetting to update indexes** - Queries fail or perform poorly
702
+ 5. **Running migrations without tracking** - May run multiple times
703
+ 6. **Removing fields before code update** - Breaks existing functionality
704
+ 7. **Not testing on development** - Production data issues
705
+
706
+ ## References
707
+
708
+ - Convex Documentation: https://docs.convex.dev/
709
+ - Convex LLMs.txt: https://docs.convex.dev/llms.txt
710
+ - Schemas: https://docs.convex.dev/database/schemas
711
+ - Database Overview: https://docs.convex.dev/database
712
+ - Migration Patterns: https://stack.convex.dev/migrate-data-postgres-to-convex
@@ -0,0 +1,3 @@
1
+ interface:
2
+ icon_small: "./assets/small-logo.svg"
3
+ icon_large: "./assets/large-logo.png"