Valet

Quickstart

Zero to running app in 5 minutes.

// App.tsx
import { ValetProvider, useQuery, useMutation } from './_generated/valet/react'
import { api } from './_generated/valet/api'

function App() {
    return (
        <ValetProvider url={process.env.VALET_PROJECT_URL!}>
            <TodoList />
        </ValetProvider>
    )
}

function TodoList() {
    const { data: todos } = useQuery(api.todos.list, {})
    const createTodo = useMutation(api.todos.create)

    return (
        <div>
            {todos?.map((todo) => (
                <div key={todo._id}>{todo.title}</div>
            ))}
            <button onClick={() => createTodo.mutate({ title: 'New todo' })}>
                Add
            </button>
        </div>
    )
}

This is what you are building toward. Follow the steps below to get there.

Install

bun add valet-dev

Initialize the project

bunx valet-dev init

This creates a valet/ directory with an example schema and functions, registers a project on the orchestrator, and saves VALET_PROJECT_URL and VALET_DEPLOY_KEY to .env.

The generated files are starter examples — replace them with your own tables and functions.

Define your schema

// valet/schema.ts
import { defineSchema, defineTable, v } from 'valet-dev/server'

export default defineSchema({
    todos: defineTable({
        title: v.string(),
        completed: v.boolean(),
    }).sync({ mode: 'full' }),
})

The schema declares your tables and their validators. Each table gets an automatic _id field.

Write a query and mutation

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

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

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

The query uses execution: 'local' so it runs against the client-side SQLite replica -- instant and offline-capable.

Run codegen

bunx valet-dev codegen --watch

This generates typed React hooks and the api object in _generated/valet/ and re-runs whenever your schema or handler files change. To run codegen once without watching, drop the --watch flag.

Use in React

Wrap your app in ValetProvider and use the generated hooks:

// App.tsx
import { ValetProvider, useQuery, useMutation } from './_generated/valet/react'
import { api } from './_generated/valet/api'

function App() {
    return (
        <ValetProvider url={process.env.VALET_PROJECT_URL!}>
            <TodoList />
        </ValetProvider>
    )
}

function TodoList() {
    const { data: todos, isLoading } = useQuery(api.todos.list, {})
    const createTodo = useMutation(api.todos.create)

    if (isLoading) return <div>Loading...</div>

    return (
        <div>
            <ul>
                {todos?.map((todo) => (
                    <li key={todo._id}>{todo.title}</li>
                ))}
            </ul>
            <button onClick={() => createTodo.mutate({ title: 'New todo' })}>
                Add Todo
            </button>
        </div>
    )
}

Next steps

  • Schema -- Tables, validators, indexes, and the _id field.
  • Queries -- Execution modes, filters, ordering, and pagination.
  • Sync Rules -- Control which documents sync to which users.
  • Auth -- JWT tokens and ctx.auth.

On this page