plans
2026-05-04
2026 05 04 Full Zod Migration

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 types

Remaining Work Summary

FileFunctionsStatusPattern
scheduled.ts1⚠️ NEEDS MIGRATIONinternalMutationzMutation
profiles.ts6⚠️ NEEDS MIGRATIONmutation/queryzMutation/zQuery
challenges.ts~30⚠️ NEEDS MIGRATIONmutation/queryzMutation/zQuery
crm.ts8⚠️ NEEDS MIGRATIONmutationzMutation
crm_sync.ts4⚠️ NEEDS MIGRATIONmutationzMutation
liveViewers.ts2⚠️ NEEDS MIGRATIONmutation/queryzMutation/zQuery
pricing.ts1⚠️ NEEDS MIGRATIONqueryzQuery
notifications.ts2⚠️ NEEDS MIGRATIONmutationzMutation

Migration Pattern

v.* to Zod Equivalents

Convex v.*Zod EquivalentNotes
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 check
  • adminMutation — wraps mutation with admin auth check
  • authenticatedQuery — wraps query with auth check
  • authenticatedMutation — wraps mutation with auth check

These will be extended with Zod variants:

  • zStaffMutation — Zod-based staff auth mutation
  • zAdminMutation — Zod-based admin auth mutation
  • zAuthenticatedQuery — Zod-based auth query
  • zAuthenticatedMutation — 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 build

Zod 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

FileFunctionsNotes
shows.ts~8Core show template CRUD
reservations.ts~10Booking flow core
occurrences.ts~6Show scheduling
addons.ts~4Add-on products
orders.ts~5Order management
tables.ts~4Table/seating
menu.ts~3Restaurant menu
analytics.ts~3Admin analytics
profiles.ts~4User profiles
booking_drafts.ts~6Booking flow state
form_sessions.ts~4Form submissions
recurrence.ts~7Recurring events
scheduled.ts~1Scheduled jobs
auth.tswrappersExtend 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 build

Rollback 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.

  1. Revert the specific function's args to v.*
  2. Change builder from zQuery/zMutation back to query/mutation
  3. No schema changes required