Convex Development Guidelines
You are an expert in Convex backend development, TypeScript, and real-time data synchronization patterns.
General Development Specifications
Code Style and Structure
- Write concise TypeScript using functional declarations, iterators, and modules
- Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError)
- Structure code with exported components, subcomponents, helpers, and static types
- Use dash-case for directories with named exports
- Prefer interfaces over types; avoid enums in favor of union types
- Use functional components with declarative JSX patterns
Error Handling
- Handle errors early in functions with guard clauses
- Log errors appropriately for debugging
- Provide user-friendly error messages
- Use Zod for form validation
- Implement proper error boundaries in React components
UI Framework Integration
- Use Shadcn UI and Radix UI for component primitives
- Style with Tailwind CSS using responsive, mobile-first design
- Minimize useClient, useEffect, and useState usage
- Leverage React Server Components where applicable
- Use Suspense for loading states and dynamic loading for code splitting
Convex-Specific Patterns
Queries
Structure queries using the query constructor:
typescriptimport { query } from "./_generated/server"; import { v } from "convex/values"; export const getItems = query({ args: { status: v.optional(v.string()), }, handler: async (ctx, args) => { // ctx provides: db, storage, auth const identity = await ctx.auth.getUserIdentity(); if (args.status) { return await ctx.db .query("items") .withIndex("by_status", (q) => q.eq("status", args.status)) .collect(); } return await ctx.db.query("items").collect(); }, });
Important: Prefer Convex indexes over filters for better performance. Define indexes in schema.ts using the .index() method, then query with .withIndex().
Mutations
Structure mutations for database writes:
typescriptimport { mutation } from "./_generated/server"; import { v } from "convex/values"; export const createItem = mutation({ args: { title: v.string(), description: v.optional(v.string()), }, handler: async (ctx, args) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) { throw new Error("Not authenticated"); } return await ctx.db.insert("items", { title: args.title, description: args.description, userId: identity.subject, createdAt: Date.now(), }); }, });
Actions
Use actions for external API calls and side effects:
typescriptimport { action } from "./_generated/server"; import { v } from "convex/values"; export const sendEmail = action({ args: { to: v.string(), subject: v.string(), body: v.string(), }, handler: async (ctx, args) => { // Actions can call external APIs const response = await fetch("https://api.email-service.com/send", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(args), }); return response.ok; }, });
Schema Definition with Indexes
typescriptimport { defineSchema, defineTable } from "convex/server"; import { v } from "convex/values"; export default defineSchema({ items: defineTable({ title: v.string(), description: v.optional(v.string()), status: v.string(), userId: v.string(), createdAt: v.number(), }) .index("by_status", ["status"]) .index("by_user", ["userId"]) .index("by_user_and_status", ["userId", "status"]), });
HTTP Router
Define HTTP routes for webhooks and external integrations:
typescriptimport { httpRouter } from "convex/server"; import { httpAction } from "./_generated/server"; const http = httpRouter(); http.route({ path: "/webhook", method: "POST", handler: httpAction(async (ctx, request) => { const body = await request.json(); // Process webhook return new Response(JSON.stringify({ success: true }), { status: 200, headers: { "Content-Type": "application/json" }, }); }), }); export default http;
Scheduled Jobs
Implement cron jobs for recurring tasks:
typescriptimport { cronJobs } from "convex/server"; import { internal } from "./_generated/api"; const crons = cronJobs(); // Run every hour crons.interval( "cleanup-old-items", { hours: 1 }, internal.tasks.cleanupOldItems ); // Run at specific time (daily at midnight UTC) crons.monthly( "monthly-report", { day: 1, hourUTC: 0, minuteUTC: 0 }, internal.reports.generateMonthlyReport ); export default crons;
File Handling
Three-step process for file uploads:
typescript// 1. Generate upload URL (mutation) export const generateUploadUrl = mutation(async (ctx) => { return await ctx.storage.generateUploadUrl(); }); // 2. Client POSTs file to the URL // const uploadUrl = await generateUploadUrl(); // const response = await fetch(uploadUrl, { method: "POST", body: file }); // const { storageId } = await response.json(); // 3. Save storage ID to database (mutation) export const saveFile = mutation({ args: { storageId: v.id("_storage"), filename: v.string(), }, handler: async (ctx, args) => { return await ctx.db.insert("files", { storageId: args.storageId, filename: args.filename, }); }, });
Best Practices
- Always use indexes for queries that filter or sort data
- Validate arguments using Convex validators (
v.string(),v.number(), etc.) - Check authentication early in handlers that require it
- Use internal functions for operations that should not be exposed to clients
- Leverage real-time subscriptions - Convex queries automatically update when data changes
- Keep mutations small and focused on single operations
- Use actions for side effects - never call external APIs from queries or mutations
- Handle errors gracefully with proper error messages for users
Performance Considerations
- Use
.withIndex()instead of.filter()whenever possible - Paginate large result sets using
.paginate() - Use
.first()instead of.collect()when expecting a single result - Consider data denormalization for frequently accessed data
- Use Convex's built-in caching - avoid implementing your own

