Skip to main content
Define your database schema to enable runtime validation and end-to-end TypeScript type safety.

Schema definition

defineSchema

Define the schema for your Convex project.
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  users: defineTable({
    name: v.string(),
    email: v.string(),
    age: v.optional(v.number()),
  }).index("by_email", ["email"]),
  
  messages: defineTable({
    text: v.string(),
    userId: v.id("users"),
    channelId: v.id("channels"),
  })
    .index("by_channel", ["channelId"])
    .index("by_user", ["userId"])
    .searchIndex("search_text", {
      searchField: "text",
    }),
});
schema
Record<string, TableDefinition>
required
A map from table name to table definition.
options
DefineSchemaOptions
schemaValidation
boolean
default:"true"
Whether Convex should validate at runtime that documents match your schema.When true, Convex will:
  1. Check that all existing documents match your schema when pushed
  2. Check that all insertions and updates match your schema
Set to false to skip runtime validation while keeping TypeScript types.
strictTableNameTypes
boolean
default:"true"
Whether TypeScript should allow accessing tables not in the schema.When true, using tables not listed in the schema generates a TypeScript error. When false, unlisted tables have document type any.Set to false for rapid prototyping.

defineTable

Define a table in your schema.
import { defineTable } from "convex/server";
import { v } from "convex/values";

const users = defineTable({
  name: v.string(),
  email: v.string(),
  verified: v.boolean(),
  joinedAt: v.number(),
});
documentSchema
PropertyValidators | Validator
required
Either an object mapping field names to validators, or a validator (for union types).
// Object style (most common)
defineTable({
  field1: v.string(),
  field2: v.number(),
})

// Validator style (for unions)
defineTable(
  v.union(
    v.object({ kind: v.literal("text"), content: v.string() }),
    v.object({ kind: v.literal("image"), url: v.string() }),
  )
)

System fields

Every document automatically has system fields that you don’t need to define:
_id
Id<TableName>
A unique document ID. Generated automatically on insert.
_creationTime
number
The timestamp (milliseconds since epoch) when the document was created.

Indexes

index

Define an index on a table for efficient queries.
defineTable({
  userId: v.id("users"),
  status: v.string(),
  createdAt: v.number(),
})
  .index("by_user", ["userId"])
  .index("by_status_createdAt", ["status", "createdAt"])
name
string
required
The name of the index. Best practice: use "by_field1_field2" naming.
fields
string[]
required
The fields to index, in order. Must specify at least one field.Index fields must be queried in the same order they’re defined. If you need to query by field2 then field1, create a separate index.
options
IndexOptions
staged
boolean
default:"false"
Whether to stage this index. Staged indexes don’t block push completion and can’t be used in queries until enabled.
Shorthand syntax:
// With options object
.index("by_user", { fields: ["userId"] })

// Array shorthand
.index("by_user", ["userId"])

searchIndex

Define a full-text search index.
defineTable({
  title: v.string(),
  content: v.string(),
  published: v.boolean(),
  category: v.string(),
})
  .searchIndex("search_content", {
    searchField: "content",
    filterFields: ["published", "category"],
  })
name
string
required
The name of the search index.
config
SearchIndexConfig
required
searchField
string
required
The field to index for full-text search. Must be a v.string() field.
filterFields
string[]
Additional fields to index for fast filtering in search queries.
staged
boolean
default:"false"
Whether to stage this index.

vectorIndex

Define a vector index for similarity search.
defineTable({
  text: v.string(),
  embedding: v.array(v.float64()),
  category: v.string(),
})
  .vectorIndex("by_embedding", {
    vectorField: "embedding",
    dimensions: 1536,
    filterFields: ["category"],
  })
name
string
required
The name of the vector index.
config
VectorIndexConfig
required
vectorField
string
required
The field containing vectors. Must be a v.array(v.float64()) field.
dimensions
number
required
The length of the vectors. Must be between 2 and 2048 inclusive.
filterFields
string[]
Additional fields to index for fast filtering in vector searches.
staged
boolean
default:"false"
Whether to stage this index.

Field validators

Use validators from convex/values to define field types:
import { v } from "convex/values";

defineTable({
  // Primitives
  name: v.string(),
  age: v.number(),
  verified: v.boolean(),
  data: v.bytes(),
  
  // Optional fields
  nickname: v.optional(v.string()),
  
  // References to other tables
  authorId: v.id("users"),
  
  // Arrays
  tags: v.array(v.string()),
  
  // Objects
  address: v.object({
    street: v.string(),
    city: v.string(),
    zip: v.string(),
  }),
  
  // Unions
  status: v.union(
    v.literal("active"),
    v.literal("inactive"),
    v.literal("pending"),
  ),
  
  // Any type (use sparingly)
  metadata: v.any(),
})

Union types

Define discriminated union types using validators:
defineTable(
  v.union(
    v.object({
      kind: v.literal("text"),
      content: v.string(),
    }),
    v.object({
      kind: v.literal("image"),
      url: v.string(),
      alt: v.string(),
    }),
    v.object({
      kind: v.literal("video"),
      url: v.string(),
      duration: v.number(),
    }),
  )
)

Schema evolution

When you modify your schema:
  1. Adding fields: Add them as optional (v.optional()) or provide a default in your code
  2. Removing fields: Remove from schema, then clean up old data with a migration
  3. Changing types: Create a new field, migrate data, then remove the old field
  4. Adding indexes: Add to schema and push. Convex will backfill automatically
  5. Removing indexes: Remove from schema and push

Best practices

Name indexes descriptively

Use "by_field1_field2" naming to make it clear what fields are indexed and in what order.

Use optional fields for flexibility

Make fields optional with v.optional() if they might not always be present. This makes schema evolution easier.

Index frequently queried fields

Create indexes for fields you filter or sort by frequently. Queries with indexes are dramatically faster.

Use validators for type safety

Validators catch bugs early by validating data at runtime and providing TypeScript types.

Stage large indexes

For large tables, use staged: true on new indexes to avoid blocking deployments during backfill.

Build docs developers (and LLMs) love