Designs Convex database schemas with TypeScript validators, indexes, and relationships. Use when creating or modifying Convex schema.ts files, defining tables, adding validators, or setting up indexes.
Comprehensive guide for creating type-safe Convex database schemas with validators, indexes, and relationships.
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
profiles: defineTable({
userId: v.string(),
slug: v.string(),
displayName: v.string(),
bio: v.optional(v.string()),
})
.index("by_user", ["userId"])
.index("by_slug", ["slug"]),
});
v.string() // String values
v.number() // Numbers (int or float)
v.boolean() // Boolean true/false
v.null() // Null value
v.id("tableName") // Reference to another table
v.optional(v.string()) // string | undefined
v.union(v.string(), v.null()) // string | null
v.union(v.string(), v.null(), v.number()) // string | null | number
v.array(v.string()) // string[]
v.array(v.object({ // Array of objects
title: v.string(),
value: v.number(),
}))
v.object({
field1: v.string(),
field2: v.number(),
nested: v.object({
deep: v.boolean(),
}),
})
// Enum-like types
v.union(
v.literal("ACTIVE"),
v.literal("INACTIVE"),
v.literal("PENDING")
)
// Mixed type unions
v.union(v.string(), v.number())
v.record(v.string(), v.boolean()) // { [key: string]: boolean }
v.record(v.string(), v.any()) // { [key: string]: any }
.index("by_slug", ["slug"])
.index("by_user", ["userId"])
Order matters! Fields must be queried in index order.
// Good: Can query by userId, or userId+timestamp
.index("by_user_time", ["userId", "timestamp"])
// Usage: ctx.db.query("table").withIndex("by_user_time", q =>
// q.eq("userId", userId).gt("timestamp", startTime)
// )
Index Query Flow:
graph LR
A[Query] --> B{Has Index?}
B -->|Yes| C[Index Lookup]
B -->|No| D[Full Table Scan]
C --> E[Fast O log n]
D --> F[Slow O n]
style C fill:#90EE90
style D fill:#FFB6C6
style E fill:#90EE90
style F fill:#FFB6C6
.searchIndex("search_profiles", {
searchField: "displayName",
filterFields: ["userId", "isActive"],
})
// Usage: ctx.db.query("profiles").withSearchIndex("search_profiles", q =>
// q.search("searchField", "john").eq("isActive", true)
// )
export default defineSchema({
profiles: defineTable({
userId: v.string(),
displayName: v.string(),
})
.index("by_user", ["userId"]),
links: defineTable({
profileId: v.id("profiles"), // Foreign key
title: v.string(),
url: v.string(),
})
.index("by_profile", ["profileId"]),
});
Schema Relationships Diagram:
erDiagram
PROFILES ||--o{ LINKS : has
PROFILES {
string _id PK
string userId
string displayName
}
LINKS {
string _id PK
id profileId FK
string title
string url
}
Make one reference nullable to allow sequential creation: