
Convex Schema Validator
- 2.7k installs
- 401 repo stars
- Updated February 6, 2026
- waynesutton/convexskills
convex-schema-validator is an agent skill for defining Convex database schemas with typed validators, indexes, optional fields, unions, and migration guidance.
About
convex-schema-validator is a Convex agent skill for authoring convex/schema.ts with defineSchema, defineTable, and convex/values validators that stay aligned with TypeScript document types. It documents validator mappings from v.string, v.number, v.id, v.optional, v.union, and v.literal through to generated types, plus index design for single-field, compound, and sort-friendly queries. Examples show users and tasks tables with references, priority unions, and channel message indexes such as by_channel and by_channel_and_time. The skill instructs agents to fetch current Convex documentation on schemas, indexes, and types instead of assuming stale API details. Migration guidance covers additive changes, backfills, and safe rollout patterns when evolving live tables. Use it when defining new tables, tightening validation, adding indexes for query paths, or planning schema changes that must not break existing Convex functions and clients.
- Covers defineSchema and defineTable patterns with convex/values validators and TypeScript alignment.
- Validator reference table maps v.string, v.optional, v.union, v.id, and related helpers to TS types.
- Index examples include single-field, compound, and time-sorted channel message queries.
- Points to official Convex schema, index, and types documentation before implementation.
- Includes migration strategies for optional fields, unions, and evolving production tables.
Convex Schema Validator by the numbers
- 2,674 all-time installs (skills.sh)
- +36 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #39 of 911 Databases skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 3, 2026 (Skillselion catalog sync)
convex-schema-validator capabilities & compatibility
- Capabilities
- defineschema and definetable authoring patterns · validator to typescript type mapping reference · single and compound index configuration · optional fields and union literal modeling · schema migration strategy guidance · official convex documentation fetch reminders
- Works with
- supabase
- Use cases
- database · api development · refactoring
What convex-schema-validator says it does
Before implementing, do not assume; fetch the latest documentation
.index("by_channel_and_time", ["channelId", "sentAt"])
npx skills add https://github.com/waynesutton/convexskills --skill convex-schema-validatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.7k |
|---|---|
| repo stars | ★ 401 |
| Security audit | 3 / 3 scanners passed |
| Last updated | February 6, 2026 |
| Repository | waynesutton/convexskills ↗ |
How do I model Convex tables with correct validators and indexes so queries stay typed and migrations do not break production data?
Define and validate Convex database schemas with typed validators, indexes, optional fields, unions, and migration strategies.
Who is it for?
Teams on Convex defining or refactoring schema.ts, indexes, and document validation rules.
Skip if: Non-Convex ORMs, raw SQL migrations, or frontend-only work without database schema changes.
When should I use this skill?
User defines Convex tables, adds indexes, uses v.optional or v.union validators, or plans schema migrations.
What you get
A validated convex/schema.ts design with indexes matched to query paths and a safe plan for schema evolution.
- schema validation report
- corrected schema definitions
Files
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
// 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` |
v.union(...) | `T1 \ | T2` |
v.literal(x) | "x" | Exact value |
v.any() | any | Any value |
v.record(k, v) | Record<K, V> | Dynamic keys |
Index Configuration
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
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
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
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:
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
// 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
// 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
// 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 after backfill
})Examples
Complete E-commerce Schema
// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
users: defineTable({
email: v.string(),
name: v.string(),
role: v.union(v.literal("customer"), v.literal("admin")),
createdAt: v.number(),
})
.index("by_email", ["email"])
.index("by_role", ["role"]),
products: defineTable({
name: v.string(),
description: v.string(),
price: v.number(),
category: v.string(),
inventory: v.number(),
isActive: v.boolean(),
})
.index("by_category", ["category"])
.index("by_active_and_category", ["isActive", "category"])
.searchIndex("search_products", {
searchField: "name",
filterFields: ["category", "isActive"],
}),
orders: defineTable({
userId: v.id("users"),
items: v.array(v.object({
productId: v.id("products"),
quantity: v.number(),
priceAtPurchase: v.number(),
})),
total: v.number(),
status: v.union(
v.literal("pending"),
v.literal("paid"),
v.literal("shipped"),
v.literal("delivered"),
v.literal("cancelled")
),
shippingAddress: v.object({
street: v.string(),
city: v.string(),
state: v.string(),
zip: v.string(),
country: v.string(),
}),
createdAt: v.number(),
updatedAt: v.number(),
})
.index("by_user", ["userId"])
.index("by_user_and_status", ["userId", "status"])
.index("by_status", ["status"]),
reviews: defineTable({
productId: v.id("products"),
userId: v.id("users"),
rating: v.number(),
comment: v.optional(v.string()),
createdAt: v.number(),
})
.index("by_product", ["productId"])
.index("by_user", ["userId"]),
});Using Schema Types in Functions
// convex/products.ts
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
import { Doc, Id } from "./_generated/dataModel";
// Use Doc type for full documents
type Product = Doc<"products">;
// Use Id type for references
type ProductId = Id<"products">;
export const get = query({
args: { productId: v.id("products") },
returns: v.union(
v.object({
_id: v.id("products"),
_creationTime: v.number(),
name: v.string(),
description: v.string(),
price: v.number(),
category: v.string(),
inventory: v.number(),
isActive: v.boolean(),
}),
v.null()
),
handler: async (ctx, args): Promise<Product | null> => {
return await ctx.db.get(args.productId);
},
});Best Practices
- Never run
npx convex deployunless explicitly instructed - Never run any git commands unless explicitly instructed
- Always define explicit schemas rather than relying on inference
- Use descriptive index names that include all indexed fields
- Start with optional fields when adding new columns
- Use discriminated unions for polymorphic data
- Validate data at the schema level, not just in functions
- Plan index strategy based on query patterns
Common Pitfalls
1. Missing indexes for queries - Every withIndex needs a corresponding schema index 2. Wrong index field order - Fields must be queried in order defined 3. Using v.any() excessively - Lose type safety benefits 4. Not making new fields optional - Breaks existing data 5. Forgetting system fields - _id and _creationTime are automatic
References
- Convex Documentation: https://docs.convex.dev/
- Convex LLMs.txt: https://docs.convex.dev/llms.txt
- Schemas: https://docs.convex.dev/database/schemas
- Indexes: https://docs.convex.dev/database/indexes
- Data Types: https://docs.convex.dev/database/types
interface:
icon_small: "./assets/small-logo.svg"
icon_large: "./assets/large-logo.png"
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_3_23)">
<g clip-path="url(#clip1_3_23)">
<path d="M10.0643 12.5735C12.3769 12.3166 14.5572 11.0843 15.7577 9.02756C15.1892 14.1148 9.62646 17.3302 5.08583 15.356C4.66743 15.1746 4.30728 14.8728 4.06013 14.4848C3.03973 12.8825 2.7043 10.8437 3.18626 8.99344C4.56327 11.37 7.3632 12.8267 10.0643 12.5735Z" fill="#F3B01C"/>
<path d="M3.1018 7.50072C2.16436 9.66714 2.12376 12.2034 3.27303 14.2907C-0.771507 11.2479 -0.72737 4.7362 3.2236 1.72378C3.58904 1.44535 4.02333 1.2801 4.47881 1.25494C6.3519 1.15614 8.25501 1.88006 9.58963 3.22909C6.87799 3.25604 4.23695 4.99308 3.1018 7.50072Z" fill="#8D2676"/>
<path d="M10.8974 3.89562C9.52924 1.98794 7.38779 0.68921 5.04156 0.649695C9.57686 -1.40888 15.1555 1.92867 15.7629 6.86314C15.8194 7.32119 15.7452 7.78824 15.5421 8.20138C14.6948 9.92223 13.1236 11.2569 11.2876 11.7508C12.6328 9.25579 12.4668 6.20748 10.8974 3.89562Z" fill="#EE342F"/>
</g>
</g>
<defs>
<clipPath id="clip0_3_23">
<rect width="16" height="16" fill="white"/>
</clipPath>
<clipPath id="clip1_3_23">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>
Related skills
How it compares
Pick convex-schema-validator for Convex schema pre-deploy checks; pick general TypeScript linters when validation does not need Convex-specific index and union rules.
FAQ
Where should I read current Convex schema rules?
Fetch docs.convex.dev/database/schemas, indexes, and types rather than assuming outdated syntax.
How do optional fields work in Convex?
Wrap validators with v.optional so documents may omit the field while TypeScript types include undefined.
When do I need compound indexes?
When queries filter or sort on multiple fields together, such as channelId plus sentAt for message timelines.
Is Convex Schema Validator safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.