Valet

Functions API

defineQuery, defineMutation, context types, and the database API.

// valet/todos.ts
import { defineQuery, defineMutation, v } from './_generated/valet/api'

export const list = defineQuery({
    args: { completed: v.optional(v.boolean()) },
    execution: 'local',
    handler: async (ctx, args) => {
        let query = ctx.db.query('todos')
        if (args.completed !== undefined) {
            query = query.filter((q) => q.eq('completed', args.completed))
        }
        return query.order('createdAt', 'desc').collect()
    },
})

export const create = defineMutation({
    args: { title: v.string() },
    handler: async (ctx, args) => {
        return ctx.db.insert('todos', {
            title: args.title,
            completed: false,
            createdAt: Date.now(),
        })
    },
})

The functions API defines queries and mutations that run against your database. Queries read data; mutations write data. Both receive a context object with a database reader or writer and optional auth information.

defineQuery

// valet/todos.ts
import { defineQuery, v } from './_generated/valet/api'

export const list = defineQuery({
    args: { limit: v.optional(v.number()) },
    execution: 'local',
    handler: async (ctx, args) => {
        return ctx.db.query('todos').take(args.limit ?? 50).collect()
    },
})

export const search = defineQuery({
    args: { query: v.string() },
    execution: 'server',
    handler: async (ctx, args) => {
        return ctx.db.search('articles', 'body', { query: args.query })
    },
})

Defines a query function. Queries have read-only access to the database.

Signature

function defineQuery<Args, Returns>(definition: QueryDefinition<Args, Returns>): QueryReference<Args, Returns>

QueryDefinition

FieldTypeRequiredDescription
argsFieldDefinitionsYesAn object of validators defining the query's arguments. Use {} for no arguments.
executionQueryExecutionModeNoWhere the query runs. Defaults to 'server'.
handler(ctx: QueryContext, args: Args) => Promise<Returns>YesThe query handler function. Receives a QueryContext and the validated arguments.

QueryExecutionMode

ValueDescription
'local'Runs against the client-side SQLite replica. Instant, offline-capable, and reactive.
'server'Runs on the server. Required for search indexes and server-only tables (sync mode 'none').
'fetch'One-shot server call. No subscription or caching -- the query runs once on the server and returns the result.

defineMutation

// valet/todos.ts
import { defineMutation, v } from './_generated/valet/api'

export const create = defineMutation({
    args: {
        title: v.string(),
        priority: v.optional(v.number()),
    },
    handler: async (ctx, args) => {
        const id = await ctx.db.insert('todos', {
            title: args.title,
            completed: false,
            priority: args.priority ?? 0,
            createdAt: Date.now(),
        })
        return id
    },
})

export const remove = defineMutation({
    args: { id: v.id('todos') },
    handler: async (ctx, args) => {
        await ctx.db.delete(args.id)
    },
})

Defines a mutation function. Mutations have read-write access to the database. When called from a client, the mutation is written to the local mutation log, applied as an optimistic update, and synced to the server. The server executes the mutation authoritatively and the result is rebased onto the client state.

Signature

function defineMutation<Args, Returns>(definition: MutationDefinition<Args, Returns>): MutationReference<Args, Returns>

MutationDefinition

FieldTypeRequiredDescription
argsFieldDefinitionsYesAn object of validators defining the mutation's arguments. Use {} for no arguments.
handler(ctx: MutationContext, args: Args) => Promise<Returns>YesThe mutation handler function. Receives a MutationContext and the validated arguments.

QueryContext

// valet/todos.ts
import { defineQuery, v } from './_generated/valet/api'

export const myTodos = defineQuery({
    args: {},
    execution: 'local',
    handler: async (ctx) => {
        // ctx.db is a DatabaseReader
        // ctx.auth contains the authenticated user's info
        if (!ctx.auth) throw new Error('Not authenticated')
        return ctx.db.query('todos')
            .filter((q) => q.eq('userId', ctx.auth!.userId))
            .collect()
    },
})

The context object passed to query handlers.

FieldTypeDescription
dbDatabaseReaderRead-only database access.
authAuthInfo | undefinedThe authenticated user's information, if a valid auth token is set.

MutationContext

// valet/todos.ts
import { defineMutation, v } from './_generated/valet/api'

export const create = defineMutation({
    args: { title: v.string() },
    handler: async (ctx, args) => {
        // ctx.db is a DatabaseWriter (extends DatabaseReader)
        // ctx.auth contains the authenticated user's info
        if (!ctx.auth) throw new Error('Not authenticated')
        return ctx.db.insert('todos', {
            title: args.title,
            completed: false,
            userId: ctx.auth.userId,
        })
    },
})

The context object passed to mutation handlers.

FieldTypeDescription
dbDatabaseWriterRead-write database access. Extends DatabaseReader.
authAuthInfo | undefinedThe authenticated user's information, if a valid auth token is set.

AuthInfo

// valet/todos.ts
import { defineQuery, v } from './_generated/valet/api'

export const me = defineQuery({
    args: {},
    execution: 'server',
    handler: async (ctx) => {
        // AuthInfo fields
        const userId: string = ctx.auth!.userId
        const claims: Record<string, unknown> | undefined = ctx.auth!.claims
        return ctx.db.query('users')
            .filter((q) => q.eq('_id', userId))
            .unique()
    },
})

Contains the authenticated user's identity, extracted from the JWT token.

FieldTypeDescription
userIdstringThe user's unique identifier (from the JWT sub claim).
claimsRecord<string, unknown> | undefinedAdditional claims from the JWT payload.

DatabaseReader

// valet/todos.ts
import { defineQuery, v } from './_generated/valet/api'

export const getById = defineQuery({
    args: { id: v.id('todos') },
    execution: 'local',
    handler: async (ctx, args) => {
        return ctx.db.get(args.id)
    },
})

export const list = defineQuery({
    args: {},
    execution: 'local',
    handler: async (ctx) => {
        return ctx.db.query('todos').collect()
    },
})

export const search = defineQuery({
    args: { query: v.string() },
    execution: 'server',
    handler: async (ctx, args) => {
        return ctx.db.search('articles', 'body', {
            query: args.query,
            limit: 20,
        })
    },
})

Read-only database access. Available in both query and mutation handlers via ctx.db.

.query(table)

Starts a query builder for the specified table. Returns a DatabaseQueryBuilder.

ctx.db.query('todos')
ParameterTypeRequiredDescription
tablestringYesThe table name to query.

Returns: DatabaseQueryBuilder<Doc>

.get(id)

Fetches a single document by its document ID. Returns null if the document does not exist.

const todo = await ctx.db.get(args.id)
ParameterTypeRequiredDescription
idId<TableName>YesThe document ID to look up.

Returns: Promise<Doc | null>

.search(table, field, options)

Runs a full-text search against a search index.

const results = await ctx.db.search('articles', 'body', {
    query: 'valet sync',
    limit: 10,
})
ParameterTypeRequiredDescription
tablestringYesThe table name to search.
fieldstringYesThe name of the search field (the field indexed with .searchIndex()).
optionsSearchOptionsYesSearch configuration.

Returns: Promise<Doc[]>

SearchOptions

FieldTypeRequiredDefaultDescription
querystringYes--The search query string.
limitnumberNo--Maximum number of results to return.

DatabaseWriter

// valet/todos.ts
import { defineMutation, v } from './_generated/valet/api'

export const create = defineMutation({
    args: { title: v.string() },
    handler: async (ctx, args) => {
        const id = await ctx.db.insert('todos', {
            title: args.title,
            completed: false,
        })
        return id
    },
})

export const update = defineMutation({
    args: { id: v.id('todos'), title: v.string() },
    handler: async (ctx, args) => {
        await ctx.db.patch(args.id, { title: args.title })
    },
})

export const replace = defineMutation({
    args: { id: v.id('todos'), title: v.string(), completed: v.boolean() },
    handler: async (ctx, args) => {
        await ctx.db.replace(args.id, {
            title: args.title,
            completed: args.completed,
        })
    },
})

export const remove = defineMutation({
    args: { id: v.id('todos') },
    handler: async (ctx, args) => {
        await ctx.db.delete(args.id)
    },
})

Read-write database access. Extends DatabaseReader with write methods. Available in mutation handlers via ctx.db.

.insert(table, document)

Inserts a new document into the table. Returns the generated document ID.

ParameterTypeRequiredDescription
tablestringYesThe table to insert into.
documentobjectYesThe document fields. Must conform to the table's validators. Do not include _id.

Returns: Promise<Id<TableName>>

.patch(id, updates)

Partially updates a document. Only the specified fields are changed; all other fields remain unchanged.

ParameterTypeRequiredDescription
idId<TableName>YesThe document ID to update.
updatesPartial<Doc>YesAn object with the fields to update.

Returns: Promise<void>

.replace(id, document)

Replaces a document entirely. The document's _id remains the same; all other fields are overwritten.

ParameterTypeRequiredDescription
idId<TableName>YesThe document ID to replace.
documentobjectYesThe complete replacement document. Must conform to the table's validators. Do not include _id.

Returns: Promise<void>

.delete(id)

Deletes a document by its document ID.

ParameterTypeRequiredDescription
idId<TableName>YesThe document ID to delete.

Returns: Promise<void>

DatabaseQueryBuilder

// valet/todos.ts
import { defineQuery, v } from './_generated/valet/api'

export const list = defineQuery({
    args: {},
    execution: 'local',
    handler: async (ctx) => {
        return ctx.db.query('todos')
            .filter((q) => q.eq('completed', false))
            .order('createdAt', 'desc')
            .take(25)
            .collect()
    },
})

export const paginated = defineQuery({
    args: { cursor: v.optional(v.string()), limit: v.optional(v.number()) },
    execution: 'local',
    handler: async (ctx, args) => {
        return ctx.db.query('todos')
            .order('createdAt', 'desc')
            .paginate({ limit: args.limit ?? 20, cursor: args.cursor })
    },
})

export const first = defineQuery({
    args: {},
    execution: 'local',
    handler: async (ctx) => {
        return ctx.db.query('todos')
            .filter((q) => q.eq('completed', false))
            .order('priority', 'desc')
            .first()
    },
})

export const byId = defineQuery({
    args: { id: v.id('todos') },
    execution: 'local',
    handler: async (ctx, args) => {
        return ctx.db.query('todos')
            .filter((q) => q.eq('_id', args.id))
            .unique()
    },
})

The query builder chains methods to filter, order, limit, and collect results. All methods return a new builder (immutable chaining) except the terminal methods (.collect(), .first(), .unique(), .paginate()).

.filter(fn)

Adds a filter predicate. The callback receives a FilterBuilder and returns a filter expression.

.filter((q) => q.eq('completed', false))
.filter((q) => q.and(q.eq('completed', false), q.gt('priority', 3)))
ParameterTypeRequiredDescription
fn(q: FilterBuilder) => FilterExpressionYesA function that builds the filter expression.

Returns: DatabaseQueryBuilder (for chaining)

FilterBuilder methods

MethodDescriptionExample
q.eq(field, value)Equal toq.eq('status', 'active')
q.neq(field, value)Not equal toq.neq('status', 'deleted')
q.lt(field, value)Less thanq.lt('priority', 5)
q.lte(field, value)Less than or equalq.lte('priority', 5)
q.gt(field, value)Greater thanq.gt('priority', 0)
q.gte(field, value)Greater than or equalq.gte('createdAt', cutoff)
q.oneOf(field, values)Value is in the arrayq.oneOf('status', ['active', 'pending'])
q.isNull(field)Field is nullq.isNull('deletedAt')
q.isNotNull(field)Field is not nullq.isNotNull('assignee')
q.arrayContains(field, value)Array field contains the valueq.arrayContains('tags', 'urgent')
q.and(...exprs)Logical ANDq.and(q.eq('a', 1), q.eq('b', 2))
q.or(...exprs)Logical ORq.or(q.eq('a', 1), q.eq('a', 2))
q.not(expr)Logical NOTq.not(q.eq('completed', true))

.order(field, direction?)

Sets the sort order for the results.

.order('createdAt', 'desc')
.order('priority')  // defaults to 'asc'
ParameterTypeRequiredDefaultDescription
fieldstringYes--The field to sort by.
direction'asc' | 'desc'No'asc'Sort direction.

Returns: DatabaseQueryBuilder (for chaining)

.take(n)

Limits the query to at most n results.

.take(10)
ParameterTypeRequiredDescription
nnumberYesMaximum number of results.

Returns: DatabaseQueryBuilder (for chaining)

.skip(n)

Skips the first n results.

.skip(20)
ParameterTypeRequiredDescription
nnumberYesNumber of results to skip.

Returns: DatabaseQueryBuilder (for chaining)

.paginate(options)

Returns a paginated result with a cursor for fetching the next page. Terminal method.

const page = await ctx.db.query('todos')
    .order('createdAt', 'desc')
    .paginate({ limit: 20, cursor: args.cursor })
ParameterTypeRequiredDescription
optionsPaginationOptionsYesPagination configuration.

Returns: Promise<PaginatedResult<T>>

.collect()

Executes the query and returns all matching documents as an array. Terminal method.

const todos = await ctx.db.query('todos').collect()

Returns: Promise<T[]>

.first()

Executes the query and returns the first matching document, or null if no documents match. Terminal method.

const todo = await ctx.db.query('todos')
    .filter((q) => q.eq('completed', false))
    .first()

Returns: Promise<T | null>

.unique()

Executes the query and returns exactly one matching document. Throws if zero or more than one document matches. Terminal method.

const todo = await ctx.db.query('todos')
    .filter((q) => q.eq('_id', args.id))
    .unique()

Returns: Promise<T>

Throws:

  • 'Query on table "X" returned no results, expected exactly one' -- no documents matched.
  • 'Query on table "X" returned multiple results, expected exactly one' -- more than one document matched.

PaginationOptions

FieldTypeRequiredDefaultDescription
limitnumberYes--Maximum number of documents per page.
cursorstringNo--The cursor from a previous PaginatedResult.nextCursor. Omit for the first page.

PaginatedResult<T>

FieldTypeDescription
dataT[]The documents in this page.
nextCursorstring | undefinedThe cursor to pass to .paginate() for the next page. undefined if there are no more pages.
hasMorebooleantrue if there are more pages after this one.

On this page