convex/schema.ts) that defines every table, every field, and every index. Convex enforces it at the database level — writes that don’t match the schema are rejected.
vly generates and updates the schema automatically as you add features. You can also edit it by hand for fine-grained control.
The basics
convex/schema.ts
defineSchema takes a map from table name to defineTable call. Each defineTable defines the fields.
Field validators
Use thev validator namespace to declare each field’s type:
A more realistic example with all of these:
convex/schema.ts
Indexes
Indexes make queries fast. Add them with.index() on the table:
convex/schema.ts
Index every field you filter or sort by
.withIndex() queries are O(log n). Without an index, queries fall back to .filter() which is O(n) — slow on large tables.Compound indexes follow query order
["ownerId", "completed"] accelerates queries that filter by ownerId (or ownerId + completed). It does not accelerate filtering by completed alone.Index naming convention
by_<field> for single-column. by_<a>_and_<b> for compound. Consistency helps query authors find the right index fast.Don't over-index
Each index slows down writes slightly. Add them when a query is slow, not preemptively.
Search indexes
For full-text search, add a search index:.withSearchIndex(). See Full-text search.
For semantic / vector search, add a vector index:
Built-in fields
Convex adds two fields to every document automatically:
These are auto-set on insert and can be queried like any other field.
_creationTime is indexable; sorting by it gives you newest-first ordering for free.
Foreign keys
Usev.id("tableName") for references between tables:
ownerId that points to an existing users row. The TypeScript type Id<"users"> is also branded, so you can’t accidentally pass a tasks ID where a users ID is expected.
How vly handles schema evolution
When you ask vly to add a field:- Updates the schema file with the new validator.
- Generates a migration script that backfills existing rows to the default.
- Updates affected queries and mutations to handle the new field.
- Updates UI components to display / edit the field.
- Runs the migration in the next deploy.
Hand-editing patterns
When you want fine control:- Tighten validation
- Add an index for a slow query
- Soft delete
Common pitfalls
Querying a non-indexed field
Querying a non-indexed field
.filter() walks every row. On a 100k-row table, this is multiple seconds. Always check for .withIndex() opportunities.Using v.any()
Using v.any()
Loses type safety. The TypeScript inference downstream becomes
any, which infects every consumer. Use a v.union(...) of literals instead, or an explicit v.object({...}).Forgetting to index foreign keys
Forgetting to index foreign keys
Whenever you query
where teamId = X, you need by_team on teamId. Foreign keys are not auto-indexed.Schema drift between branches
Schema drift between branches
If two branches add different fields to the same table, merging is messy. vly uses version history for snapshotting, but for big schema changes, prefer to land them sequentially.
Next
Queries
Read patterns: indexes, filters, ordering, pagination.
Mutations
Transactional writes.
Indexes
Deep dive on index design.
