Install Zero

This guide walks you through adding Zero to any TypeScript-based web app.

It should take about 20 minutes to complete. When you're done, you'll have Zero up and running and will understand its core ideas.

Integrate Zero

Set Up Your Database

You'll need a local Postgres database for development. If you don't have a preferred method, we recommend using Docker:

docker run -d --name zero-postgres \
  -e POSTGRES_PASSWORD="password" \
  -p 5432:5432 \
  postgres:16-alpine \
  # IMPORTANT: logical WAL level is required for Zero
  # to sync data to its SQLite replica
  postgres -c wal_level=logical

This will start a Postgres database running in the background.

Install and Run Zero-Cache

Add Zero to your project:

npm install @rocicorp/zero

Start the development zero-cache by running the following command:

export ZERO_UPSTREAM_DB="postgres://postgres:password@localhost:5432/postgres"
npx zero-cache-dev

Zero works by continuously replicating your upstream database into a SQLite replica.

Zero-cache runs client queries against the replica. If there are tables or columns that will not be queried by Zero clients ever, you can exclude them.

You can use the zero-sqlite3 tool to explore zero.db. Try it out by connecting to Postgres and the Zero replica in two different terminals. If you change something in Postgres, you'll see it immediately show up in the replica:

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 schema.ts automatically. Otherwise, you can create it manually.

npm install -D drizzle-zero
npx drizzle-zero generate

Set Up the Zero Client

Zero has first-class support for React and SolidJS, and community support for Svelte and Vue.

There is also a low-level API you can use in any TypeScript-based project.

// root.tsx
import {ZeroProvider} from '@rocicorp/zero/react'
import type {ZeroOptions} from '@rocicorp/zero'
import {schema} from './zero/schema.ts'
 
const opts: ZeroOptions = {
  userID: 'anon',
  cacheURL: 'http://localhost:4848',
  schema
}
 
function Root() {
  return (
    <ZeroProvider {...opts}>
      <App />
    </ZeroProvider>
  )
}
 
// mycomponent.tsx
import {useZero} from '@rocicorp/zero/react'
 
function MyComponent() {
  const zero = useZero()
  console.log('clientID', zero.clientID)
}

Sync Data

Define Query

Alright, let's sync some data!

In Zero, we do this with queries. Queries are conventionally found in a queries.ts file. Here is an example of how queries are defined - you can adapt this to your own schema:

// zero/queries.ts
import {defineQueries, defineQuery} from '@rocicorp/zero'
import {z} from 'zod'
import {zql} from './schema.ts'
 
export const queries = defineQueries({
  albums: {
    byArtist: defineQuery(
      z.object({artistID: z.string()}),
      ({args: {artistID}}) =>
        zql.albums
          .where('artistId', artistID)
          .orderBy('createdAt', 'asc')
          .limit(10)
          .related('artist', q => q.one())
    )
  }
})

Use zql from schema.ts to construct and return a ZQL query. ZQL is quite powerful and allows you to build queries with filters, sorts, relationships, and more:

See queries for more information on defining queries.

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:

// mycomponent.tsx
import {useQuery} from '@rocicorp/zero/react'
import {queries} from './zero/queries.ts'
 
function MyComponent() {
  const [albums] = useQuery(
    queries.albums.byArtist({artistID: 'artist_1'})
  )
  return albums.map(a => <div key={a.id}>{a.title}</div>)
}

When you reload your app, you should see an error like:

> npx zero-cache-dev
...
no ZERO_QUERY_URL is configured for Zero Cache

This is expected. We now need to implement a queries endpoint so that zero-cache can get the ZQL for the albums.byArtist query.

Implement Query Backend

Zero doesn't allow clients to run any arbitrary ZQL against zero-cache, for both security and performance reasons.

Instead, Zero sends the name and arguments of the query to a queries endpoint on your server that is responsible for transforming the named query into ZQL.

Zero provides utilities to make it easy to implement the queries endpoint in any full-stack framework:

// 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.ts'
import {schema} from '../../zero/schema.ts'
 
export const Route = createFileRoute('/api/query')({
  server: {
    handlers: {
      POST: async ({request}) => {
        const result = await handleQueryRequest(
          (name, args) => {
            const query = mustGetQuery(queries, name)
            return query.fn({args, ctx: {userId: 'anon'}})
          },
          schema,
          request
        )
 
        return Response.json(result)
      }
    }
  }
})

Stop and re-run zero-cache with the URL of the queries endpoint:

export ZERO_UPSTREAM_DB="postgres://postgres:password@localhost:5432/postgres"
export ZERO_QUERY_URL="http://localhost:3000/api/query"
npx zero-cache-dev

If you reload the page, you will see data! Zero queries update live, so if you edit data in Postgres directly, you will see it update in the Zero replica AND the UI:

More about Queries

You now know the basics, but there are a few more important pieces you'll need to learn for your first real app:

For these details and more, see Reading Data.

But for now, let's move on to writes!

Mutate Data

Define Mutators

Data is written in Zero apps using mutators. Similar to queries, we use a shared mutators.ts file:

// zero/mutators.ts
import {defineMutators, defineMutator} from '@rocicorp/zero'
import {z} from 'zod'
 
export const mutators = defineMutators({
  albums: {
    create: defineMutator(
      z.object({
        id: z.string(),
        artistID: z.string(),
        title: z.string(),
        year: z.number(),
        createdAt: z.number()
      }),
      async ({args, tx}) => {
        await tx.mutate.albums.insert({
          id: args.id,
          artistId: args.artistID,
          title: args.title,
          releaseYear: args.year,
          createdAt: args.createdAt
        })
      }
    )
  }
})

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.

Once you've defined your mutators, you must register them with Zero before you can use them:

import {mutators} from './zero/mutators.ts'
 
const opts: ZeroOptions = {
  // ... userID, cacheURL, etc.
  // add mutators
  mutators
}

Invoke Mutators

You can now call mutators via zero.mutate:

// mycomponent.tsx
import {useZero} from '@rocicorp/zero/react'
import {mutators} from './zero/mutators.ts'
import {nanoid} from 'nanoid'
 
function MyComponent() {
  const zero = useZero()
 
  const onClick = async () => {
    const result = zero.mutate(
      mutators.albums.create({
        id: nanoid(),
        artistID: 'artist_1',
        title: 'Please Please Me',
        year: 1963,
        createdAt: Date.now()
      })
    )
 
    const clientResult = await result.client
 
    if (clientResult.type === 'error') {
      console.error(
        'Failed to create album',
        clientResult.error.message
      )
    } else {
      console.log('Album created!')
    }
  }
 
  return <button onClick={onClick}>Create Album</button>
}

If you run this app now, you should be able to see the UI update optimistically, but you'll also see an error in zero-cache:

> npx zero-cache-dev
...
A ZERO_MUTATE_URL must be set in order to process mutations

Similar to queries, we need to wire up a mutate endpoint in our API. Let's do that now.

Implement Mutate Endpoint

Zero requires a mutate endpoint which runs on your server and connects directly to your Postgres database. Zero provides helpers to implement this easily.

Use the Zero Postgres adapters to create a dbProvider instance:

// src/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 '../../zero/schema.ts'
import * as drizzleSchema from '../../drizzle/schema.ts'
 
// pass a drizzle client instance. for example:
const pool = new Pool({
  connectionString: process.env.ZERO_UPSTREAM_DB!
})
export const drizzleClient = drizzle(pool, {
  schema: drizzleSchema
})
export const dbProvider = zeroDrizzle(schema, drizzleClient)
 
// Register the database provider for type safety
declare module '@rocicorp/zero' {
  interface DefaultTypes {
    dbProvider: typeof dbProvider
  }
}

Then, use the dbProvider to handle the mutate request:

// 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.ts'
import {dbProvider} from '../../db-provider.ts'
 
export const Route = createFileRoute('/api/mutate')({
  server: {
    handlers: {
      POST: async ({request}) => {
        const result = await handleMutateRequest(
          dbProvider,
          transact =>
            transact((tx, name, args) => {
              const mutator = mustGetMutator(mutators, name)
              return mutator.fn({
                args,
                tx,
                ctx: {userId: 'anon'}
              })
            }),
          request
        )
 
        return Response.json(result)
      }
    }
  }
})

Restart zero-cache to tell it about this new endpoint:

export ZERO_UPSTREAM_DB="postgres://postgres:password@localhost:5432/postgres"
export ZERO_QUERY_URL="http://localhost:3000/api/query"
export ZERO_MUTATE_URL="http://localhost:3000/api/mutate"
npx zero-cache-dev

If you refresh the page, your mutation should commit to the database and sync to other clients:

More about Mutators

Just as with queries, the separate server implementation of mutators extends elegantly to enable write permissions. Zero also has built-in helpers to do work after a mutator runs on the server, like send notifications.

For these details and more, see Writing Data.

That's It!

Congratulations! You now know the basics for building with Zero 🤯.

Possible next steps: