export default defineSchema({ items: defineTable({ // Optional: field may not exist description: v.optional(v.string()), // Nullable: field exists but can be null deletedAt: v.union(v.number(), v.null()), // Optional and nullable notes: v.optional(v.union(v.string(), v.
Index Naming Convention
Always include all indexed fields in the index name:
// Beforeusers: defineTable({ name: v.string(), email: v.string(),})// After - add as optional firstusers: defineTable({ name: v.string(), email: v.string(), avatarUrl: v.optional(v.string()), // New optional field})
Backfilling Data
// convex/migrations.tsimport { internalMutation } from "./_generated/server";import { v } from "convex/values";export const backfillAvatars = internalMutation({ args: {}, returns: v.number(), handler: async (ctx) => {
Making Optional Fields Required
// Step 1: Backfill all null values// Step 2: Update schema to requiredusers: defineTable({ name: v.string(), email: v.string(), avatarUrl: v.string(), // Now required after backfill})
Examples
Complete E-commerce Schema
// convex/schema.tsimport { defineSchema, defineTable } from "convex/server";import { v } from "convex/values";export default
Using Schema Types in Functions
// convex/products.tsimport { query, mutation } from "./_generated/server";import { v } from "convex/values";import { Doc, Id } from "./_generated/dataModel";// Use Doc type for full documentstype Product = Doc<"products">;// Use Id type for references
Best Practices
Never run npx convex deploy unless explicitly instructed
Never run any git commands unless explicitly instructed
Always define explicit schemas rather than relying on inference
Use descriptive index names that include all indexed fields
Start with optional fields when adding new columns
Use discriminated unions for polymorphic data
Validate data at the schema level, not just in functions
Plan index strategy based on query patterns
Common Pitfalls
Missing indexes for queries - Every withIndex needs a corresponding schema index
Wrong index field order - Fields must be queried in order defined
Using v.any() excessively - Lose type safety benefits
Not making new fields optional - Breaks existing data
Forgetting system fields - _id and _creationTime are automatic