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.
| Order | Window | Safe? |
|---|---|---|
| Convex first (current pipeline) | Old frontend calls old functions on new backend | Yes, with backwards compatibility |
| Frontend first | New frontend calls non-existent functions on old backend | Immediately 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:
- Deploy 1: Add the new field, write to both old and new, read from new with fallback to old
- Deploy 2: (after confirming all clients use the new field) Stop writing the old field
- Deploy 3: Remove the old field from the schema
Deprecating a query or mutation
Don't delete it immediately. Two-phase:
- Deploy 1: Deploy the new frontend that no longer calls the old function
- 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:
- Add the new field (optional) alongside the old one
- Backfill existing documents (via a migration mutation)
- Update code to read from new field
- Remove the old field once all documents are migrated
Summary
| Action | Safe to deploy directly? | Notes |
|---|---|---|
| Add new query/mutation | Yes | Old frontend ignores it |
| Add optional field | Yes | Existing docs unaffected |
| Add required field | No | Existing docs will fail validation — add as optional first |
| Change function args | No | Add new args as optional, keep old signature working |
| Rename field | No | Two-phase: add new, migrate, remove old |
| Delete query/mutation | No | Remove callers first, then delete function |
| Change field type | No | Add new field, migrate data, remove old field |