| name | source-command-convex-schema-builder |
| description | Design Convex database schemas with proper validation, indexes, and relationship patterns |
source-command-convex-schema-builder
Use this skill when the user asks to run the migrated source command convex-schema-builder.
Command Template
Convex Schema Builder
Design schemas for the Convex document-relational database. Schemas are defined in convex/schema.ts.
Schema Basics
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
tableName: defineTable({
field: v.string(),
}).index("by_field", ["field"]),
});
Validator Reference
v.string();
v.number();
v.boolean();
v.int64();
v.null();
v.bytes();
v.id("tableName");
v.array(v.string());
v.object({ key: v.string() });
v.optional(v.string());
v.union(v.literal("a"), v.literal("b"));
v.record(v.string(), v.number());
v.any();
From convex-helpers/validators:
import { nullable, literals, partial } from "convex-helpers/validators";
nullable(v.string());
literals("a", "b", "c");
partial(myObjectValidator);
Relationship Patterns
One-to-Many
Store the foreign key on the "many" side with an index:
users: defineTable({
name: v.string(),
email: v.string(),
}),
tasks: defineTable({
userId: v.id("users"),
title: v.string(),
completed: v.boolean(),
})
.index("by_userId", ["userId"]),
Many-to-Many (Junction Table)
users: defineTable({
name: v.string(),
}),
teams: defineTable({
name: v.string(),
}),
teamMembers: defineTable({
userId: v.id("users"),
teamId: v.id("teams"),
role: v.union(v.literal("member"), v.literal("admin")),
joinedAt: v.number(),
})
.index("by_userId", ["userId"])
.index("by_teamId", ["teamId"])
.index("by_teamId_and_userId", ["teamId", "userId"]),
Self-Referential (Hierarchical)
categories: defineTable({
name: v.string(),
parentId: v.optional(v.id("categories")),
depth: v.number(),
})
.index("by_parentId", ["parentId"]),
Index Strategy
Rules:
- Every foreign key field needs an index
- Name indexes with all fields:
by_fieldA_and_fieldB
- Compound indexes cover prefix queries (e.g.,
by_userId_and_status covers queries on just userId)
- Don't create redundant indexes
Common patterns:
tasks: defineTable({
userId: v.id("users"),
status: v.union(v.literal("active"), v.literal("completed")),
priority: v.number(),
createdAt: v.number(),
})
.index("by_userId", ["userId"])
.index("by_userId_and_status", ["userId", "status"])
.index("by_status", ["status"]),
Using indexes in queries:
await ctx.db
.query("tasks")
.withIndex("by_userId", (q) => q.eq("userId", userId))
.collect();
await ctx.db
.query("tasks")
.withIndex("by_userId_and_status", (q) =>
q.eq("userId", userId).eq("status", "active"),
)
.collect();
await ctx.db
.query("tasks")
.withIndex("by_userId_and_status", (q) =>
q.eq("userId", userId).gte("status", "a"),
)
.collect();
Schema Design Rules
- Flat documents - avoid deeply nested objects. Use separate tables with IDs
- Arrays for small, bounded data - max 8192 items. For unbounded lists, use a separate table
- Timestamps as numbers - use
v.number() with Date.now(), not date strings
- Enums as unions - use
v.union(v.literal(...)) pattern
- Optional for nullable - use
v.optional() for fields that may not exist
- No circular references - design schema as a DAG
Anti-Patterns
Bad - deeply nested:
users: defineTable({
posts: v.array(
v.object({
comments: v.array(
v.object({
text: v.string(),
replies: v.array(v.object({ text: v.string() })),
}),
),
}),
),
});
Good - flat with relationships:
users: defineTable({ name: v.string() }),
posts: defineTable({ userId: v.id("users"), text: v.string() })
.index("by_userId", ["userId"]),
comments: defineTable({ postId: v.id("posts"), userId: v.id("users"), text: v.string() })
.index("by_postId", ["postId"]),
Checklist
- Define all tables in
convex/schema.ts
- Add validators for every field
- Create indexes for all foreign keys
- Use compound indexes for common query patterns
- Name indexes descriptively:
by_fieldA_and_fieldB
- Keep documents flat - use IDs for relationships
- Use
v.int64() not v.bigint()
- Run
npx convex codegen after changes