Full Zod Migration — Convex Function Args
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Migrate all Convex backend function args from v.* validators to Zod schemas using convex-helpers/server/zod3. Eliminates the api as any type tax where Convex codegen generates FunctionReference<any, "public"> losing argument/return type specificity.
Architecture: Replace v.* validators with Zod equivalents in function args. Custom builders (zQuery, zMutation, zAction) already exist in lib/zod.ts. Auth wrappers (staffMutation, adminMutation, authenticatedQuery, authenticatedMutation) in auth.ts will be extended with Zod-based variants.
Tech Stack: Convex 1.37.0, Zod, convex-helpers 0.1.115, TypeScript
File Structure — UPDATED
packages/backend/convex/
├── lib/
│ └── zod.ts # Already created: zQuery, zMutation, zAction, z, zid, withSystemFields
├── functions/
│ ├── shows.ts # ✅ Already migrated (zQuery/zMutation)
│ ├── reservations.ts # ✅ Already migrated (zQuery/zMutation)
│ ├── occurrences.ts # ✅ Already migrated (zQuery/zMutation)
│ ├── addons.ts # ✅ Already migrated (zQuery/zMutation)
│ ├── orders.ts # ✅ Already migrated (zQuery/zMutation)
│ ├── tables.ts # ✅ Already migrated (zQuery/zMutation)
│ ├── menu.ts # ✅ Already migrated (zQuery/zMutation)
│ ├── analytics.ts # ✅ Already migrated (zQuery)
│ ├── booking_drafts.ts # ✅ Already migrated (zMutation)
│ ├── form_sessions.ts # ✅ Already migrated (zMutation)
│ ├── auth.ts # ✅ Already has Zod wrappers
│ ├── checkIns.ts # ✅ Already migrated
│ ├── payments.ts # ✅ Already migrated
│ ├── recurrence.ts # ✅ Only utility functions, no Convex functions
│ ├── scheduled.ts # ⚠️ NEEDS MIGRATION (internalMutation → zMutation)
│ ├── profiles.ts # ⚠️ NEEDS MIGRATION (mutation/query → zMutation/zQuery)
│ ├── challenges.ts # ⚠️ NEEDS MIGRATION (mutation/query → zMutation/zQuery)
│ ├── crm.ts # ⚠️ NEEDS MIGRATION (mutation → zMutation)
│ ├── crm_sync.ts # ⚠️ NEEDS MIGRATION (mutation → zMutation)
│ ├── liveViewers.ts # ⚠️ NEEDS MIGRATION (mutation/query → zMutation/zQuery)
│ ├── pricing.ts # ⚠️ NEEDS MIGRATION (query → zQuery)
│ ├── notifications.ts # ⚠️ NEEDS MIGRATION (mutation → zMutation)
│ ├── shows.http.ts # ✅ Already uses Zod
│ ├── booking.http.ts # ✅ Already uses Zod
│ ├── tickets.http.ts # ✅ Already uses Zod
│ ├── vnpay.http.ts # ✅ Already uses Zod
│ └── onePay.http.ts # ✅ Already uses Zod (but has workaround casts)
└── schema.ts # Source of truth for Doc typesRemaining Work Summary
| File | Functions | Status | Pattern |
|---|---|---|---|
scheduled.ts | 1 | ⚠️ NEEDS MIGRATION | internalMutation → zMutation |
profiles.ts | 6 | ⚠️ NEEDS MIGRATION | mutation/query → zMutation/zQuery |
challenges.ts | ~30 | ⚠️ NEEDS MIGRATION | mutation/query → zMutation/zQuery |
crm.ts | 8 | ⚠️ NEEDS MIGRATION | mutation → zMutation |
crm_sync.ts | 4 | ⚠️ NEEDS MIGRATION | mutation → zMutation |
liveViewers.ts | 2 | ⚠️ NEEDS MIGRATION | mutation/query → zMutation/zQuery |
pricing.ts | 1 | ⚠️ NEEDS MIGRATION | query → zQuery |
notifications.ts | 2 | ⚠️ NEEDS MIGRATION | mutation → zMutation |
Migration Pattern
v.* to Zod Equivalents
| Convex v.* | Zod Equivalent | Notes |
|---|---|---|
v.string() | z.string() | — |
v.number() | z.number() | — |
v.boolean() | z.boolean() | — |
v.null() | z.null() | — |
v.id("table") | zid("table") | From convex-helpers/server/zod3 |
v.optional(v.string()) | z.string().optional() | Zod chainable |
v.array(v.string()) | z.array(z.string()) | — |
v.union(v.literal("A"), v.literal("B")) | z.enum(["A", "B"]) | More ergonomic |
v.object({ ... }) | z.object({ ... }) | — |
v.nullable(v.string()) | z.string().nullable() | Zod chainable |
Example Migration
Before (v.* validators):
export const createReservation = mutation({
args: {
occurrenceId: v.id("showOccurrences"),
customerEmail: v.string(),
customerName: v.string(),
quantity: v.number(),
totalAmount: v.number(),
},
handler: async (
ctx,
{ occurrenceId, customerEmail, customerName, quantity, totalAmount },
) => {
// ...
},
});After (Zod):
export const createReservation = zMutation({
args: z.object({
occurrenceId: zid("showOccurrences"),
customerEmail: z.string().email(),
customerName: z.string().min(2),
quantity: z.number().int().positive(),
totalAmount: z.number().nonnegative(),
}),
handler: async (
ctx,
{ occurrenceId, customerEmail, customerName, quantity, totalAmount },
) => {
// Fully typed — no cast needed
},
});Auth Wrappers Extension
The existing auth.ts has:
staffMutation— wraps mutation with staff auth checkadminMutation— wraps mutation with admin auth checkauthenticatedQuery— wraps query with auth checkauthenticatedMutation— wraps mutation with auth check
These will be extended with Zod variants:
zStaffMutation— Zod-based staff auth mutationzAdminMutation— Zod-based admin auth mutationzAuthenticatedQuery— Zod-based auth queryzAuthenticatedMutation— Zod-based auth mutation
Task Breakdown
Task 1: Extend auth.ts with Zod Variants
Files:
-
Modify:
packages/backend/convex/functions/auth.ts -
Step 1: Read existing auth.ts to understand current wrappers
cat packages/backend/convex/functions/auth.ts- Step 2: Add Zod-based auth wrapper functions
import {
customMutation,
customQuery,
} from "convex-helpers/server/customFunctions";
import { zMutation, zQuery, z } from "../lib/zod";
// Staff mutation with Zod args validation
export function zStaffMutation<Args extends z.ZodTypeAny>(
args: Args,
handler: MutationHandler<MutationCtx, z.infer<Args>>,
) {
return staffMutation({
args: args as unknown as Record<string, Validator>,
handler: handler as any,
});
}- Step 3: Verify TypeScript compilation
cd packages/backend/convex && npx tsc --noEmit- Step 4: Commit
Task 2: Migrate shows.ts
Files:
-
Modify:
packages/backend/convex/functions/shows.ts -
Functions: ~8 (listActive, getBySlug, create, update, archive, etc.)
-
Step 1: Read shows.ts
cat packages/backend/convex/functions/shows.ts- Step 2: Migrate first query (listActive) to zQuery
import { zQuery, z } from "../lib/zod";
import { z } from "zod";
export const listActive = zQuery({
args: z.object({}),
handler: async (ctx) => {
return await ctx.db
.query("showTemplates")
.withIndex("by_status", (q) => q.eq("status", "ACTIVE"))
.collect();
},
});- Step 3: Migrate getBySlug to zQuery with z.object for args
export const getBySlug = zQuery({
args: z.object({ slug: z.string() }),
handler: async (ctx, { slug }) => {
const show = await ctx.db
.query("showTemplates")
.withIndex("by_slug", (q) => q.eq("slug", slug))
.unique();
return show;
},
});- Step 4: Migrate create mutation with zid and z.enum
export const create = zMutation({
args: z.object({
title: z.string().min(1).max(200),
slug: z.string().regex(/^[a-z0-9-]+$/),
description: z.string(),
duration: z.number().positive(),
status: z.enum(["ACTIVE", "DRAFT", "ARCHIVED"]).default("DRAFT"),
pricing: z.record(z.string(), z.number()),
}),
handler: async (ctx, args) => {
const id = await ctx.db.insert("showTemplates", args);
return id;
},
});- Step 5: Verify build
cd packages/backend/convex && npx convex dev --once- Step 6: Commit
Task 3: Migrate occurrences.ts
Files:
-
Modify:
packages/backend/convex/functions/occurrences.ts -
Functions: ~6 (listByTemplate, upcoming, generateBatch, etc.)
-
Step 1: Read occurrences.ts
cat packages/backend/convex/functions/occurrences.ts- Step 2: Migrate queries to zQuery
export const listByTemplate = zQuery({
args: z.object({ templateId: zid("showTemplates") }),
handler: async (ctx, { templateId }) => {
return await ctx.db
.query("showOccurrences")
.withIndex("by_template", (q) => q.eq("templateId", templateId))
.collect();
},
});- Step 3: Migrate generateBatch mutation
export const generateBatch = zMutation({
args: z.object({
templateId: zid("showTemplates"),
startDate: z.string(),
endDate: z.string(),
timeSlots: z.array(
z.object({
time: z.string(),
capacity: z.number().int().positive(),
}),
),
}),
handler: async (ctx, args) => {
// Implementation...
},
});- Step 4: Verify and commit
Task 4: Migrate reservations.ts
Files:
-
Modify:
packages/backend/convex/functions/reservations.ts -
Functions: ~10 (createPending, confirmPayment, cancel, listByOccurrence, etc.)
-
Step 1: Read reservations.ts
cat packages/backend/convex/functions/reservations.ts- Step 2: Migrate createPending mutation with proper validation
export const createPending = zMutation({
args: z.object({
occurrenceId: zid("showOccurrences"),
customerEmail: z.string().email(),
customerName: z.string().min(2),
customerPhone: z.string(),
quantity: z.number().int().min(1).max(10),
totalAmount: z.number().nonnegative(),
bundleType: z.enum(["DINNER_THEATRE", "SHOW_ONLY"]).optional(),
addonIds: z.array(zid("addOns")).optional(),
}),
handler: async (ctx, args) => {
// Implementation with availability check...
},
});- Step 3: Migrate confirmPayment mutation
export const confirmPayment = zMutation({
args: z.object({
reservationId: zid("reservations"),
paymentProvider: z.enum(["VNPAY", "ONEPAY"]),
transactionRef: z.string(),
}),
handler: async (ctx, { reservationId, paymentProvider, transactionRef }) => {
// Implementation...
},
});- Step 4: Verify and commit
Task 5: Migrate addons.ts
Files:
-
Modify:
packages/backend/convex/functions/addons.ts -
Functions: ~4 (list, create, update, delete)
-
Step 1: Read addons.ts
cat packages/backend/convex/functions/addons.ts-
Step 2: Migrate all functions to zQuery/zMutation
-
Step 3: Verify and commit
Task 6: Migrate orders.ts
Files:
-
Modify:
packages/backend/convex/functions/orders.ts -
Functions: ~5
-
Step 1: Read orders.ts
cat packages/backend/convex/functions/orders.ts-
Step 2: Migrate to Zod
-
Step 3: Verify and commit
Task 7: Migrate tables.ts
Files:
-
Modify:
packages/backend/convex/functions/tables.ts -
Functions: ~4
-
Step 1: Read tables.ts
cat packages/backend/convex/functions/tables.ts-
Step 2: Migrate to Zod
-
Step 3: Verify and commit
Task 8: Migrate menu.ts
Files:
-
Modify:
packages/backend/convex/functions/menu.ts -
Functions: ~3
-
Step 1: Read menu.ts
cat packages/backend/convex/functions/menu.ts-
Step 2: Migrate to Zod
-
Step 3: Verify and commit
Task 9: Migrate analytics.ts
Files:
-
Modify:
packages/backend/convex/functions/analytics.ts -
Functions: ~3 queries
-
Step 1: Read analytics.ts
cat packages/backend/convex/functions/analytics.ts-
Step 2: Migrate to zQuery
-
Step 3: Verify and commit
Task 10: Migrate profiles.ts
Files:
-
Modify:
packages/backend/convex/functions/profiles.ts -
Functions: ~4
-
Step 1: Read profiles.ts
cat packages/backend/convex/functions/profiles.ts-
Step 2: Migrate to Zod (likely uses authenticatedQuery wrapper)
-
Step 3: Verify and commit
Task 11: Migrate booking_drafts.ts
Files:
-
Modify:
packages/backend/convex/functions/booking_drafts.ts -
Functions: ~6
-
Step 1: Read booking_drafts.ts
cat packages/backend/convex/functions/booking_drafts.ts-
Step 2: Migrate to zMutation with session-based auth
-
Step 3: Verify and commit
Task 12: Migrate form_sessions.ts
Files:
-
Modify:
packages/backend/convex/functions/form_sessions.ts -
Functions: ~4 mutations (CONTACT, VENUE_RENTAL, PRIVATE_EVENTS, etc.)
-
Step 1: Read form_sessions.ts
cat packages/backend/convex/functions/form_sessions.ts- Step 2: Migrate v.union(v.literal(...)) to z.enum([])
// Before
args: {
formType: v.union(
v.literal("CONTACT"),
v.literal("VENUE_RENTAL"),
v.literal("PRIVATE_EVENTS"),
v.literal("WORKSHOPS"),
v.literal("ARTIST_PROPOSAL"),
v.literal("HOST_AN_EVENT"),
),
}
// After
args: z.object({
formType: z.enum(["CONTACT", "VENUE_RENTAL", "PRIVATE_EVENTS", "WORKSHOPS", "ARTIST_PROPOSAL", "HOST_AN_EVENT"]),
}),- Step 3: Verify and commit
Task 13: Migrate recurrence.ts
Files:
-
Modify:
packages/backend/convex/functions/recurrence.ts -
Functions: ~7 (3 queries, 4 mutations)
-
Step 1: Read recurrence.ts
cat packages/backend/convex/functions/recurrence.ts- Step 2: Migrate complex day parsing logic to Zod
// Before
args: {
pattern: v.union(
v.literal("WEEKLY"),
v.literal("BIWEEKLY"),
v.literal("MONTHLY"),
v.literal("CUSTOM"),
),
days: v.array(v.string()), // ["MON", "WED", "FRI"]
}
// After
args: z.object({
pattern: z.enum(["WEEKLY", "BIWEEKLY", "MONTHLY", "CUSTOM"]),
days: z.array(z.enum(["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"])),
}),- Step 3: Verify and commit
Task 14: Migrate scheduled.ts
Files:
-
Modify:
packages/backend/convex/functions/scheduled.ts -
Functions: ~1 internal mutation (d1LowOccupancyCheck)
-
Step 1: Read scheduled.ts
cat packages/backend/convex/functions/scheduled.ts- Step 2: Migrate to zMutation (internalMutation equivalent)
export const d1LowOccupancyCheck = zMutation({
args: z.object({}),
handler: async (ctx) => {
// Implementation...
},
});- Step 3: Verify and commit
Task 15: Verify HTTP Handlers Don't Need Changes
Files:
-
packages/backend/convex/functions/shows.http.ts— Already uses Zod -
packages/backend/convex/functions/booking.http.ts— Already uses Zod -
packages/backend/convex/functions/tickets.http.ts— Already uses Zod -
packages/backend/convex/functions/vnpay.http.ts— Already uses Zod -
packages/backend/convex/functions/onePay.http.ts— Already uses Zod -
Step 1: Check onePay.http.ts for workaround casts (FunctionReference cast)
grep -n "FunctionReference" packages/backend/convex/functions/onePay.http.ts- Step 2: If workaround casts exist, document but don't fix (separate issue)
The FunctionReference cast workaround in HTTP handlers is a known pattern. The proper fix requires further investigation and is out of scope for this migration.
Task 16: Final Verification
- Step 1: Run full TypeScript check
cd packages/backend/convex && npx tsc --noEmit- Step 2: Run convex dev to verify all functions deploy
cd packages/backend/convex && npx convex dev --once- Step 3: Verify frontend builds without api as any errors
cd apps/frontend && npm run buildZod Migration Reference
Essential Imports
// From lib/zod.ts (already created)
import {
zQuery,
zMutation,
zAction,
z,
zid,
withSystemFields,
} from "../lib/zod";
// From zod directly
import { z } from "zod";
// From convex-helpers for special validators
import { zid, withSystemFields } from "convex-helpers/server/zod3";Common Zod Transformations
// Email validation
z.string().email();
// Min/max length
z.string().min(2).max(100);
// Positive number
z.number().positive();
// Non-negative number
z.number().nonnegative();
// Integer
z.number().int();
// Optional with default
z.enum(["A", "B"]).default("A");
// Nullable
z.string().nullable();
// Array of IDs
z.array(zid("showOccurrences"));Output Validation (Queries)
export const listShows = zQuery({
args: z.object({}),
returns: z.array(
z.object(
withSystemFields("showTemplates", {
title: z.string(),
slug: z.string(),
status: z.enum(["ACTIVE", "DRAFT", "ARCHIVED"]),
}),
),
),
handler: async (ctx) => {
return await ctx.db.query("showTemplates").collect();
},
});Files Summary
| File | Functions | Notes |
|---|---|---|
shows.ts | ~8 | Core show template CRUD |
reservations.ts | ~10 | Booking flow core |
occurrences.ts | ~6 | Show scheduling |
addons.ts | ~4 | Add-on products |
orders.ts | ~5 | Order management |
tables.ts | ~4 | Table/seating |
menu.ts | ~3 | Restaurant menu |
analytics.ts | ~3 | Admin analytics |
profiles.ts | ~4 | User profiles |
booking_drafts.ts | ~6 | Booking flow state |
form_sessions.ts | ~4 | Form submissions |
recurrence.ts | ~7 | Recurring events |
scheduled.ts | ~1 | Scheduled jobs |
auth.ts | wrappers | Extend with Zod variants |
| Total | ~65 |
Dependencies
{
"convex": "^1.37.0",
"convex-helpers": "^0.1.115",
"zod": "^3.25.0"
}No new dependencies required — convex-helpers with Zod support is already available.
Verification Commands
# TypeScript check
cd packages/backend/convex && npx tsc --noEmit
# Convex deployment check
cd packages/backend/convex && npx convex dev --once
# Frontend build (verifies api types)
cd apps/frontend && npm run buildRollback Plan
If issues arise, rollback is straightforward — each function can independently fall back to v.* validators. The Zod migration is additive at the function level.
- Revert the specific function's args to
v.* - Change builder from
zQuery/zMutationback toquery/mutation - No schema changes required