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-devInitialize the project
bunx valet-dev initThis 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 --watchThis 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
_idfield. - Queries -- Execution modes, filters, ordering, and pagination.
- Sync Rules -- Control which documents sync to which users.
- Auth -- JWT tokens and
ctx.auth.