All docs/general

docs/architecture/convex-deployment-guidelines.md

Convex Deployment Guidelines

Deploy Order: Convex First, Then Frontend

The CI/CD pipeline deploys Convex before Vercel apps. This follows the official Convex recommendation: deploy backend functions first, then build and deploy the frontend against the updated backend.

OrderWindowSafe?
Convex first (current pipeline)Old frontend calls old functions on new backendYes, with backwards compatibility
Frontend firstNew frontend calls non-existent functions on old backendImmediately broken

npx convex deploy is atomic — either the full new version goes live, or the deploy fails and the old version stays. A failed deploy is safe.

Backwards Compatibility Is Mandatory

Between Convex deploying and Vercel catching up, users may still be running the old frontend against the new backend. Convex functions must remain backwards compatible for at least one deploy cycle.

This is true even outside CI/CD — users with cached browser tabs will always be one version behind.

Common Scenarios

Adding a new query or mutation

Safe. The old frontend doesn't call it, so it sits idle until the new frontend deploys.

// New query — no backwards compatibility concern
export const getAnalytics = query({
  args: { flowId: v.id("flows") },
  handler: async (ctx, args) => { ... },
});

Adding a new field to the schema

Use optional fields first. Deploy the schema change, then deploy the code that writes/reads the field.

// Step 1: Add as optional
defineTable({
  name: v.string(),
  description: v.optional(v.string()), // new field, optional
});

// Step 2 (next deploy): Start writing the field
// Step 3 (later): Make required once all docs have it

Changing a function's arguments

Add new args as optional. Keep the old signature working.

// Before
export const getFlow = query({
  args: { flowId: v.id("flows") },
  handler: async (ctx, { flowId }) => { ... },
});

// After — old callers still work
export const getFlow = query({
  args: {
    flowId: v.id("flows"),
    includeAnalytics: v.optional(v.boolean()), // new, optional
  },
  handler: async (ctx, { flowId, includeAnalytics }) => {
    // Handle both old (no arg) and new (with arg) callers
    ...
  },
});

Renaming a field

Never rename in place. Use a two-phase approach:

  1. Deploy 1: Add the new field, write to both old and new, read from new with fallback to old
  2. Deploy 2: (after confirming all clients use the new field) Stop writing the old field
  3. Deploy 3: Remove the old field from the schema

Deprecating a query or mutation

Don't delete it immediately. Two-phase:

  1. Deploy 1: Deploy the new frontend that no longer calls the old function
  2. Deploy 2: Remove the function from Convex (now safe — no callers)

Schema migrations

Convex won't let you deploy a schema that conflicts with existing data. For type changes:

  1. Add the new field (optional) alongside the old one
  2. Backfill existing documents (via a migration mutation)
  3. Update code to read from new field
  4. Remove the old field once all documents are migrated

Summary

ActionSafe to deploy directly?Notes
Add new query/mutationYesOld frontend ignores it
Add optional fieldYesExisting docs unaffected
Add required fieldNoExisting docs will fail validation — add as optional first
Change function argsNoAdd new args as optional, keep old signature working
Rename fieldNoTwo-phase: add new, migrate, remove old
Delete query/mutationNoRemove callers first, then delete function
Change field typeNoAdd new field, migrate data, remove old field