Schema API
defineSchema, defineTable, validators, indexes, and sync configuration.
// valet/schema.ts
import { defineSchema, defineTable, v } from 'valet-dev/server'
export default defineSchema({
todos: defineTable({
title: v.string(),
completed: v.boolean(),
priority: v.number(),
userId: v.id('users'),
})
.index('by_priority', ['priority'])
.sync({
filter: (q, ctx) => q.eq('userId', ctx.auth.userId),
mode: 'full',
}),
users: defineTable({
name: v.string(),
email: v.string(),
})
.index('by_email', ['email'])
.searchIndex('search_users', { searchField: 'name' }),
})The schema API defines your database tables, validators, indexes, and sync configuration. Every Valet project has a single schema.ts file in the valet/ directory that exports the result of defineSchema().
defineSchema
// valet/schema.ts
import { defineSchema, defineTable, v } from 'valet-dev/server'
export default defineSchema({
todos: defineTable({
title: v.string(),
completed: v.boolean(),
}),
users: defineTable({
name: v.string(),
}),
})Compiles table definitions into a schema object. The schema is consumed by codegen to produce typed hooks, API references, and schema.json.
Signature
function defineSchema<T extends SchemaDefinition>(tables: T): CompiledSchema<T>Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
tables | SchemaDefinition | Yes | An object where each key is a table name and each value is a TableBuilder from defineTable(). |
Returns
A CompiledSchema<T> object containing the compiled table definitions, validators, indexes, and sync configuration.
defineTable
// valet/schema.ts
import { defineSchema, defineTable, v } from 'valet-dev/server'
export default defineSchema({
messages: defineTable({
body: v.string(),
authorId: v.id('users'),
channelId: v.id('channels'),
sentAt: v.number(),
}),
})Defines a single table with its field validators. Returns a TableBuilder that you chain with .index(), .searchIndex(), .sync(), and .backfill().
Signature
function defineTable<T extends FieldDefinitions>(fields: T): TableBuilder<T>Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
fields | FieldDefinitions | Yes | An object where each key is a field name and each value is a validator from the v namespace. |
Every table automatically gets an _id field of type Id<"tableName">. Do not declare _id in the field definitions.
TableBuilder methods
.index(name, fields)
// valet/schema.ts
import { defineSchema, defineTable, v } from 'valet-dev/server'
export default defineSchema({
todos: defineTable({
title: v.string(),
completed: v.boolean(),
priority: v.number(),
createdAt: v.number(),
})
.index('by_priority', ['priority'])
.index('by_completed_and_created', ['completed', 'createdAt']),
})Adds a database index for efficient filtering and ordering.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | A unique name for the index. |
fields | string[] | Yes | An ordered array of field names to include in the index. Compound indexes support queries filtering on these fields in order. |
Returns the TableBuilder for chaining.
.searchIndex(name, config)
// valet/schema.ts
import { defineSchema, defineTable, v } from 'valet-dev/server'
export default defineSchema({
articles: defineTable({
title: v.string(),
body: v.string(),
category: v.string(),
}).searchIndex('search_articles', {
searchField: 'body',
}),
})Adds a full-text search index on a string field.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | A unique name for the search index. |
config.searchField | string | Yes | The string field to index for full-text search. |
Returns the TableBuilder for chaining.
.sync(config)
// valet/schema.ts
import { defineSchema, defineTable, v } from 'valet-dev/server'
export default defineSchema({
// Full sync with a filter: each user only syncs their own todos
todos: defineTable({
title: v.string(),
userId: v.id('users'),
}).sync({
filter: (q, ctx) => q.eq('userId', ctx.auth.userId),
mode: 'full',
}),
// Windowed sync: only sync messages from the last 7 days
messages: defineTable({
body: v.string(),
sentAt: v.number(),
}).sync({
filter: (q, ctx) => q.gte('sentAt', 0),
mode: 'windowed',
window: { field: 'sentAt', duration: 7 * 24 * 60 * 60 * 1000 },
}),
// No sync: server-only table
analytics: defineTable({
event: v.string(),
timestamp: v.number(),
}).sync({ mode: 'none' }),
})Configures the sync filter for a table. The sync filter determines which documents are synced to each client.
| Parameter | Type | Required | Description |
|---|---|---|---|
config | SyncConfig | Yes | The sync configuration object. |
SyncConfig
| Field | Type | Required | Description |
|---|---|---|---|
filter | SyncFilterFn | Depends on mode | A function (q, ctx) => FilterExpr that returns a filter expression determining which documents sync to this client. Required for 'full' and 'windowed' modes. Must not be set for 'none' mode. |
mode | SyncMode | Yes | The execution mode for sync. |
window | WindowConfig | Only for 'windowed' | Time window configuration. Required when mode is 'windowed'. |
SyncMode
| Value | Description |
|---|---|
'full' | Sync all matching documents to the client. The sync filter determines which documents match. |
'windowed' | Sync only documents within a time window. Combines the sync filter with a time-based constraint. |
'none' | Do not sync this table. Documents are only accessible via server-side queries. |
WindowConfig
| Field | Type | Required | Description |
|---|---|---|---|
field | string | Yes | The numeric field to use as the time axis (typically a timestamp in milliseconds). |
duration | number | Yes | The window size in milliseconds. Documents older than this are excluded from sync. |
SyncFilterFn
type SyncFilterFn<Doc> = (
q: TypedSyncFilterBuilder<Doc>,
ctx: SyncContext
) => FilterExprThe filter function receives a query builder for constructing filter expressions and a context object with auth information. Return a filter expression that determines which documents sync to the client.
SyncContext
| Field | Type | Description |
|---|---|---|
auth | SyncAuthContext | The authenticated user's information. |
Returns the TableBuilder for chaining.
.backfill(column, fn)
// valet/schema.ts
import { defineSchema, defineTable, v } from 'valet-dev/server'
export default defineSchema({
todos: defineTable({
title: v.string(),
completed: v.boolean(),
priority: v.number(),
}).backfill('priority', () => 0),
})Registers a backfill function for a newly added column. When codegen pushes the schema to the server, the server runs this function against every existing row that has a null value for the column.
| Parameter | Type | Required | Description |
|---|---|---|---|
column | string | Yes | The name of the column to backfill. Must be a field defined in the table. |
fn | (doc: Document) => FieldValue | Yes | A function that receives the existing document and returns the value to set for the new column. |
Returns the TableBuilder for chaining.
Validators (v namespace)
// valet/schema.ts
import { defineSchema, defineTable, v } from 'valet-dev/server'
export default defineSchema({
tasks: defineTable({
title: v.string(),
count: v.number(),
position: v.int(),
completed: v.boolean(),
attachment: v.bytes(),
deletedAt: v.null(),
assignee: v.id('users'),
tags: v.array(v.string()),
metadata: v.object({ source: v.string() }),
description: v.optional(v.string()),
status: v.union(v.literal('active'), v.literal('done')),
}),
users: defineTable({
name: v.string(),
}),
})The v namespace provides validator constructors for defining field types. Import v from 'valet-dev/server'.
Primitive validators
| Validator | TypeScript Type | Description |
|---|---|---|
v.string() | string | UTF-8 string. |
v.number() | number | 64-bit floating-point number (IEEE 754). |
v.int() | number | 64-bit integer. Rejects floats at runtime. |
v.boolean() | boolean | true or false. |
v.bytes() | ArrayBuffer | Arbitrary binary data. |
v.null() | null | The value null. |
Reference validators
| Validator | TypeScript Type | Description |
|---|---|---|
v.id("tableName") | Id<"tableName"> | A document ID referencing a row in the specified table. Branded string at compile time, plain string at runtime. |
Compound validators
| Validator | TypeScript Type | Description |
|---|---|---|
v.array(element) | T[] | An array where each element matches the inner validator. |
v.object(fields) | { ... } | A nested object with the specified field validators. |
v.optional(inner) | T | undefined | The field may be absent. Wraps any other validator. |
v.union(...variants) | T1 | T2 | ... | The value must match one of the provided validators. |
v.literal(value) | Exact value type | The value must be exactly the provided string, number, or boolean. |
.deprecated()
// valet/schema.ts
import { defineSchema, defineTable, v } from 'valet-dev/server'
export default defineSchema({
todos: defineTable({
title: v.string(),
completed: v.boolean(),
// Old field -- still in the database, hidden from generated types
done: v.boolean().deprecated(),
}),
})Chain .deprecated() on any validator to mark the field as deprecated. The field remains in the database and continues to validate, but codegen excludes it from the generated TypeScript types. Use this to phase out fields without a breaking schema change.
Signature
validator.deprecated(): DeprecatedValidatorWorks on any validator: v.string().deprecated(), v.optional(v.number()).deprecated(), etc.
Id<TableName>
// valet/todos.ts
import { defineQuery, defineMutation, v } from './_generated/valet/api'
import type { Id } from 'valet-dev/server'
export const get = defineQuery({
args: { id: v.id('todos') },
execution: 'local',
handler: async (ctx, args) => {
// args.id is Id<"todos"> -- a branded string
return ctx.db.get(args.id)
},
})
export const remove = defineMutation({
args: { id: v.id('todos') },
handler: async (ctx, args) => {
await ctx.db.delete(args.id)
},
})Id<TableName> is a branded string type. At runtime it is a plain string. At compile time it carries the table name, preventing you from passing an Id<"users"> where an Id<"todos"> is expected.
Codegen produces Id types for each table in api.d.ts. You can also import the generic Id type from 'valet-dev/server'.
| Property | Value |
|---|---|
| Runtime type | string |
| Compile-time type | string & { __tableName: TableName } |
| Generated by | db.insert() return value, doc._id field |
| Validated by | v.id("tableName") |
Converting from string
Id<TableName> is branded at compile time only — at runtime it is a plain string. When you have a plain string from an external source (URL params, local storage, a third-party API), cast it to the appropriate Id type with as:
import type { Id } from './_generated/valet/api'
const todoId = params.id as Id<'todos'>
const todo = await ctx.db.get(todoId)Going the other direction is automatic — Id<"todos"> is assignable to string with no cast needed.