schema-builder
Design and generate Convex database schemas with proper validation, indexes, and relationships. Use when creating schema.ts or modifying table definitions.
Savant verdict: Review advised
Security analysis or source-hub review identified patterns requiring attention.
Live evaluation
Not evaluated yet. Workspaces can request a live evaluation.
Security assessment
Scan pending.
Structure
- No license declaredConfirm you may reuse this skill before importing it into your repository.
- The source hub's security checks flagged this skillRunlayer: warn (1/1 file flagged)
SKILL.md
---
name: schema-builder
description: Design and generate Convex database schemas with proper validation, indexes, and relationships. Use when creating schema.ts or modifying table definitions.
---
# Convex Schema Builder
Build well-structured Convex schemas following best practices for relationships, indexes, and validators.
## When to Use
- Creating a new `convex/schema.ts` file
- Adding tables to existing schema
- Designing data model relationships
- Adding or optimizing indexes
- Converting nested data to relational structure
## Schema Design Principles
1. **Document-Relational**: Use flat documents with ID references, not deep nesting
2. **Index Foreign Keys**: Always index fields used in lookups (userId, teamId, etc.)
3. **Limit Arrays**: Only use arrays for small, bounded collections (<8192 items)
4. **Type Safety**: Use strict validators with `v.*` types
## Schema Template
```typescript
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
tableName: defineTable({
// Required fields
field: v.string(),
// Optional fields
optional: v.optional(v.number()),
// Relations (use IDs)
userId: v.id("users"),
// Enums with union + literal
status: v.union(
v.literal("active"),
v.literal("pending"),
v.literal("archived")
),
// Timestamps
createdAt: v.number(),
updatedAt: v.optional(v.number()),
})
// Index for queries by this field
.index("by_user", ["userId"])
// Compound index for common query patterns
.index("by_user_and_status", ["userId", "status"])
// Index for time-based queries
.index("by_created", ["createdAt"]),
});
```
## Common Patterns
### One-to-Many Relationship
```typescript
export default defineSchema({
users: defineTable({
name: v.string(),
email: v.string(),
}).index("by_email", ["email"]),
posts: defineTable({
userId: v.id("users"),
title: v.string(),
content: v.string(),
}).index("by_user", ["userId"]),
});
```
### Many-to-Many with Junction Table
```typescript
export default defineSchema({
users: defineTable({
name: v.string(),
}),
projects: defineTable({
name: v.string(),
}),
projectMembers: defineTable({
userId: v.id("users"),
projectId: v.id("projects"),
role: v.union(v.literal("owner"), v.literal("member")),
})
.index("by_user", ["userId"])
.index("by_project", ["projectId"])
.index("by_project_and_user", ["projectId", "userId"]),
});
```
### Hierarchical Data
```typescript
export default defineSchema({
comments: defineTable({
postId: v.id("posts"),
parentId: v.optional(v.id("comments")), // null for top-level
userId: v.id("users"),
text: v.string(),
})
.index("by_post", ["postId"])
.index("by_parent", ["parentId"]),
});
```
### Small Bounded Arrays (OK to use)
```typescript
export default defineSchema({
users: defineTable({
name: v.string(),
// Small, bounded collections are fine
roles: v.array(v.union(
v.literal("admin"),
v.literal("editor"),
v.literal("viewer")
)),
tags: v.array(v.string()), // e.g., max 10 tags
}),
});
```
## Validator Reference
```typescript
// Primitives
v.string()
v.number()
v.boolean()
v.null()
v.id("tableName")
// Optional
v.optional(v.string())
// Union types (enums)
v.union(v.literal("a"), v.literal("b"))
// Objects
v.object({
key: v.string(),
nested: v.number(),
})
// Arrays
v.array(v.string())
// Records (arbitrary keys)
v.record(v.string(), v.boolean())
// Any (avoid if possible)
v.any()
```
## Index Strategy
1. **Single-field indexes**: For simple lookups
- `by_user: ["userId"]`
- `by_email: ["email"]`
2. **Compound indexes**: For filtered queries
- `by_user_and_status: ["userId", "status"]`
- `by_team_and_created: ["teamId", "createdAt"]`
3. **Remove redundant**: `by_a_and_b` usually covers `by_a`
## Checklist
- [ ] All foreign keys have indexes
- [ ] Common query patterns have compound indexes
- [ ] Arrays are small and bounded (or converted to relations)
- [ ] All fields have proper validators
- [ ] Enums use `v.union(v.literal(...))` pattern
- [ ] Timestamps use `v.number()` (milliseconds since epoch)
## Migration from Nested to Relational
If converting from nested structures:
**Before:**
```typescript
users: defineTable({
posts: v.array(v.object({
title: v.string(),
comments: v.array(v.object({ text: v.string() })),
})),
})
```
**After:**
```typescript
users: defineTable({
name: v.string(),
}),
posts: defineTable({
userId: v.id("users"),
title: v.string(),
}).index("by_user", ["userId"]),
comments: defineTable({
postId: v.id("posts"),
text: v.string(),
}).index("by_post", ["postId"]),
```