convex-schema-validator
The convex-schema-validator skill provides a complete framework for defining and validating Convex database schemas with TypeScript typing, field validation, index configuration, and union types. Use this when building Convex applications to establish typed database tables with proper indexing strategies, optional fields, relationships between documents, and composite data validation rules.
git clone --depth 1 https://github.com/waynesutton/convexskills /tmp/convex-schema-validator && cp -r /tmp/convex-schema-validator/skills/convex-schema-validator ~/.claude/skills/convex-schema-validatorSKILL.md
# Convex Schema Validator
Define and validate database schemas in Convex with proper typing, index configuration, optional fields, unions, and strategies for schema migrations.
## Documentation Sources
Before implementing, do not assume; fetch the latest documentation:
- Primary: https://docs.convex.dev/database/schemas
- Indexes: https://docs.convex.dev/database/indexes
- Data Types: https://docs.convex.dev/database/types
- For broader context: https://docs.convex.dev/llms.txt
## Instructions
### Basic Schema Definition
```typescript
// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
users: defineTable({
name: v.string(),
email: v.string(),
avatarUrl: v.optional(v.string()),
createdAt: v.number(),
}),
tasks: defineTable({
title: v.string(),
description: v.optional(v.string()),
completed: v.boolean(),
userId: v.id("users"),
priority: v.union(
v.literal("low"),
v.literal("medium"),
v.literal("high")
),
}),
});
```
### Validator Types
| Validator | TypeScript Type | Example |
|-----------|----------------|---------|
| `v.string()` | `string` | `"hello"` |
| `v.number()` | `number` | `42`, `3.14` |
| `v.boolean()` | `boolean` | `true`, `false` |
| `v.null()` | `null` | `null` |
| `v.int64()` | `bigint` | `9007199254740993n` |
| `v.bytes()` | `ArrayBuffer` | Binary data |
| `v.id("table")` | `Id<"table">` | Document reference |
| `v.array(v)` | `T[]` | `[1, 2, 3]` |
| `v.object({})` | `{ ... }` | `{ name: "..." }` |
| `v.optional(v)` | `T \| undefined` | Optional field |
| `v.union(...)` | `T1 \| T2` | Multiple types |
| `v.literal(x)` | `"x"` | Exact value |
| `v.any()` | `any` | Any value |
| `v.record(k, v)` | `Record<K, V>` | Dynamic keys |
### Index Configuration
```typescript
export default defineSchema({
messages: defineTable({
channelId: v.id("channels"),
authorId: v.id("users"),
content: v.string(),
sentAt: v.number(),
})
// Single field index
.index("by_channel", ["channelId"])
// Compound index
.index("by_channel_and_author", ["channelId", "authorId"])
// Index for sorting
.index("by_channel_and_time", ["channelId", "sentAt"]),
// Full-text search index
articles: defineTable({
title: v.string(),
body: v.string(),
category: v.string(),
})
.searchIndex("search_content", {
searchField: "body",
filterFields: ["category"],
}),
});
```
### Complex Types
```typescript
export default defineSchema({
// Nested objects
profiles: defineTable({
userId: v.id("users"),
settings: v.object({
theme: v.union(v.literal("light"), v.literal("dark")),
notifications: v.object({
email: v.boolean(),
push: v.boolean(),
}),
}),
}),
// Arrays of objects
orders: defineTable({
customerId: v.id("users"),
items: v.array(v.object({
productId: v.id("products"),
quantity: v.number(),
price: v.number(),
})),
status: v.union(
v.literal("pending"),
v.literal("processing"),
v.literal("shipped"),
v.literal("delivered")
),
}),
// Record type for dynamic keys
analytics: defineTable({
date: v.string(),
metrics: v.record(v.string(), v.number()),
}),
});
```
### Discriminated Unions
```typescript
export default defineSchema({
events: defineTable(
v.union(
v.object({
type: v.literal("user_signup"),
userId: v.id("users"),
email: v.string(),
}),
v.object({
type: v.literal("purchase"),
userId: v.id("users"),
orderId: v.id("orders"),
amount: v.number(),
}),
v.object({
type: v.literal("page_view"),
sessionId: v.string(),
path: v.string(),
})
)
).index("by_type", ["type"]),
});
```
### Optional vs Nullable Fields
```typescript
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.null())),
}),
});
```
### Index Naming Convention
Always include all indexed fields in the index name:
```typescript
export default defineSchema({
posts: defineTable({
authorId: v.id("users"),
categoryId: v.id("categories"),
publishedAt: v.number(),
status: v.string(),
})
// Good: descriptive names
.index("by_author", ["authorId"])
.index("by_author_and_category", ["authorId", "categoryId"])
.index("by_category_and_status", ["categoryId", "status"])
.index("by_status_and_published", ["status", "publishedAt"]),
});
```
### Schema Migration Strategies
#### Adding New Fields
```typescript
// Before
users: defineTable({
name: v.string(),
email: v.string(),
})
// After - add as optional first
users: defineTable({
name: v.string(),
email: v.string(),
avatarUrl: v.optional(v.string()), // New optional field
})
```
#### Backfilling Data
```typescript
// convex/migrations.ts
import { internalMutation } from "./_generated/server";
import { v } from "convex/values";
export const backfillAvatars = internalMutation({
args: {},
returns: v.number(),
handler: async (ctx) => {
const users = await ctx.db
.query("users")
.filter((q) => q.eq(q.field("avatarUrl"), undefined))
.take(100);
for (const user of users) {
await ctx.db.patch(user._id, {
avatarUrl: `https://api.dicebear.com/7.x/initials/svg?seed=${user.name}`,
});
}
return users.length;
},
});
```
#### Making Optional Fields Required
```typescript
// Step 1: Backfill all null values
// Step 2: Update schema to required
users: defineTable({
name: v.string(),
email: v.string(),
avatarUrl: v.string(), // Now required afterPrevent feature creep when building software, apps, and AI-powered products. Use this skill when planning features, reviewing scope, building MVPs, managing backlogs, or when a user says "just one more feature." Helps developers and AI agents stay focused, ship faster, and avoid bloated products.
Building AI agents with the Convex Agent component including thread management, tool integration, streaming responses, RAG patterns, and workflow orchestration
Guidelines for building production-ready Convex apps covering function organization, query patterns, validation, TypeScript usage, error handling, and the Zen of Convex design philosophy
How to create, structure, and publish self-contained Convex components with proper isolation, exports, and dependency management
Scheduled function patterns for background tasks including interval scheduling, cron expressions, job monitoring, retry strategies, and best practices for long-running tasks
Complete file handling including upload flows, serving files via URL, storing generated files from actions, deletion, and accessing file metadata from system tables
Writing queries, mutations, actions, and HTTP actions with proper argument validation, error handling, internal functions, and runtime considerations
External API integration and webhook handling including HTTP endpoint routing, request/response handling, authentication, CORS configuration, and webhook signature validation