Install Zero
This guide shows how to add Zero to an existing TypeScript-based web app. For a concrete end-to-end walkthrough, build the music app in the tutorial.
Integrate Zero
Set Up Your Database
You'll need a Postgres database with logical replication enabled for development.
# IMPORTANT: logical WAL level is required for Zero
# to sync data to its SQLite replica
docker run -d --name zero-postgres \
-e POSTGRES_DB="zero" \
-e POSTGRES_PASSWORD="pass" \
-p 5432:5432 \
postgres:18 \
postgres -c wal_level=logicalCreate a .env file so your app server and zero-cache-dev use the same Postgres connection:
# Update to your app's database connection URL
ZERO_UPSTREAM_DB="postgres://postgres:pass@localhost:5432/zero"Install Zero
Add Zero and the validator used in these examples:
npm install @rocicorp/zero zodThese examples use Zod; any Standard Schema-compatible validator works.
Set Up Your Zero Schema
Zero uses a file called schema.ts to provide a type-safe query API.
If you use Drizzle or Prisma, you can generate the schema automatically. Otherwise, you can create it manually.
npm install -D drizzle-zero
npx drizzle-zero generate --output src/zero/schema.tsSet Up the Zero Client
Zero has first-class support for React and SolidJS, and there is also a low-level API you can use in any TypeScript-based project. Choose the tab that most closely matches where your app creates its root layout or client instance.
// src/routes/__root.tsx
import {ZeroProvider} from '@rocicorp/zero/react'
import type {ZeroOptions} from '@rocicorp/zero'
import {
HeadContent,
Scripts,
createRootRoute
} from '@tanstack/react-router'
import type {ReactNode} from 'react'
import {schema} from '../zero/schema'
const opts: ZeroOptions = {
cacheURL: 'http://localhost:4848',
schema
}
export const Route = createRootRoute({
shellComponent: RootDocument
})
function RootDocument({children}: {children: ReactNode}) {
return (
<html lang="en">
<head>
<HeadContent />
</head>
<body>
<ZeroProvider {...opts}>{children}</ZeroProvider>
<Scripts />
</body>
</html>
)
}Sync Data
Define Query
Shared reads are conventionally stored in queries.ts. Use zql from schema.ts to construct and return a ZQL query:
// src/zero/queries.ts
import {defineQueries, defineQuery} from '@rocicorp/zero'
import {zql} from './schema'
export const queries = defineQueries({
allUsers: defineQuery(() => zql.user)
})See Reading Data for more on filters, sorting, relationships, and permissions.
Add Query Endpoint
Zero doesn't allow clients to send arbitrary ZQL to zero-cache.
Instead, Zero sends the query name and arguments to the query endpoint on your server, which responds to zero-cache with the authoritative ZQL. This prevents clients from reading arbitrary data and is the basis of permissions.
// src/routes/api/query.ts
import {createFileRoute} from '@tanstack/react-router'
import {handleQueryRequest} from '@rocicorp/zero/server'
import {mustGetQuery} from '@rocicorp/zero'
import {queries} from '../../zero/queries'
import {schema} from '../../zero/schema'
export const Route = createFileRoute('/api/query')({
server: {
handlers: {
POST: async ({request}) => {
const result = await handleQueryRequest({
handler: (name, args) => {
const query = mustGetQuery(queries, name)
return query.fn({args})
},
schema,
request,
userID: null
})
return Response.json(result)
}
}
}
})Invoke Query
Querying for data is framework-specific. Most of the time, you will use a helper like useQuery that integrates into your framework's rendering model:
import {useQuery} from '@rocicorp/zero/react'
import {queries} from './zero/queries'
const [users] = useQuery(queries.allUsers())More about Queries
Mutate Data
Define Mutators
Data is written in Zero apps using mutators. Similar to queries, shared writes usually live in mutators.ts:
// src/zero/mutators.ts
import {defineMutators, defineMutator} from '@rocicorp/zero'
import {z} from 'zod'
export const mutators = defineMutators({
activateUser: defineMutator(
z.object({id: z.string()}),
async ({args: {id}, tx}) => {
await tx.mutate.user.update({id, active: true})
}
)
})You can use the CRUD-style API with tx.mutate.<table>.<method>() to write data. You can also use tx.run(zql.<table>.<method>) to run queries within your mutator.
Register the mutators where you create the Zero client:
// src/routes/__root.tsx
import type {ZeroOptions} from '@rocicorp/zero'
import {mutators} from '../zero/mutators'
import {schema} from '../zero/schema'
const opts: ZeroOptions = {
cacheURL: 'http://localhost:4848',
schema,
mutators
}Add Mutate Endpoint
Zero requires a mutate endpoint that runs on your server and connects directly to Postgres.
First, create a dbProvider with the Postgres adapter that matches your stack. These examples assume the selected database client is already installed in your app.
// src/zero/db-provider.ts
import {zeroDrizzle} from '@rocicorp/zero/server/adapters/drizzle'
import {drizzle} from 'drizzle-orm/node-postgres'
import {Pool} from 'pg'
import {schema} from './schema'
import * as drizzleSchema from '../drizzle/schema'
// If your app uses a different Drizzle driver, reuse your existing client.
const connectionString = process.env.ZERO_UPSTREAM_DB
if (!connectionString) {
throw new Error('ZERO_UPSTREAM_DB is not set')
}
const pool = new Pool({
connectionString
})
export const drizzleClient = drizzle(pool, {
schema: drizzleSchema
})
export const dbProvider = zeroDrizzle(schema, drizzleClient)
// Register global types for mutators on the server
declare module '@rocicorp/zero' {
interface DefaultTypes {
dbProvider: typeof dbProvider
}
}Then use the dbProvider and helpers to define the mutate endpoint:
// src/routes/api/mutate.ts
import {createFileRoute} from '@tanstack/react-router'
import {handleMutateRequest} from '@rocicorp/zero/server'
import {mustGetMutator} from '@rocicorp/zero'
import {mutators} from '../../zero/mutators'
import {dbProvider} from '../../zero/db-provider'
export const Route = createFileRoute('/api/mutate')({
server: {
handlers: {
POST: async ({request}) => {
const result = await handleMutateRequest({
dbProvider,
handler: transact =>
transact((tx, name, args) => {
const mutator = mustGetMutator(mutators, name)
return mutator.fn({args, tx})
}),
request,
userID: null
})
return Response.json(result)
}
}
}
})Mutators on the server allow for write permissions and can be different from the client implementation. You can also do work after a mutation runs on the server, like send notifications.
Start your app server in another terminal, then run zero-cache locally with ZERO_QUERY_URL and ZERO_MUTATE_URL configured. If your app uses a different origin, update localhost:3000.
ZERO_QUERY_URL="http://localhost:3000/api/query" \
ZERO_MUTATE_URL="http://localhost:3000/api/mutate" \
npx zero-cache-devInvoke Mutators
You can call a mutator with zero.mutate:
import {useZero} from '@rocicorp/zero/react'
import {mutators} from './zero/mutators'
const zero = useZero()
const onClick = () => {
zero.mutate(mutators.activateUser({id: '1'}))
}When you run the mutator, Zero writes to the local database, updates queries optimistically, and then syncs in the background to your mutate endpoint.
Your mutate endpoint writes to Postgres and zero-cache will instantly replicate those changes to other clients.