# 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](https://zero.rocicorp.dev/docs/tutorial).

## Integrate Zero

### Set Up Your Database

You'll need a Postgres database with logical replication enabled for development.

**Docker**

```bash
# 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=logical
```

**Postgres.app**

```bash
# Start Postgres.app first. Requires Postgres 15 or higher.
# If these already exist, you can skip those commands.
createuser -s postgres
createdb -O postgres zero

psql -d postgres -c "ALTER USER postgres WITH PASSWORD 'pass';"
psql -d postgres -c "ALTER SYSTEM SET wal_level = 'logical';"

# Restart Postgres.app, then verify:
psql -d postgres -c "SHOW wal_level;"
```

> 🧑‍💻 **Already using another Postgres provider?**: See [Provider Support](https://zero.rocicorp.dev/docs/connecting-to-postgres) and make sure `wal_level` is `logical`.

Create a `.env` file so your app server and `zero-cache-dev` use the same Postgres connection:

```bash
# 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**

```bash
npm install @rocicorp/zero zod
```

**pnpm**

```bash
pnpm add @rocicorp/zero zod

# Note: pnpm disables postinstall scripts by default for security.
# Create or update pnpm-workspace.yaml to allow the native package build:
# https://pnpm.io/settings#allowbuilds
# allowBuilds:
#   '@rocicorp/zero-sqlite3': true
pnpm rebuild @rocicorp/zero-sqlite3
```

**bun**

```bash
bun add @rocicorp/zero zod

# Note: Bun disables postinstall scripts by default for security.
# Either approve the build:
bun pm trust @rocicorp/zero-sqlite3

# Or add to package.json, then rebuild the native packages:
# "trustedDependencies": ["@rocicorp/zero-sqlite3"]
```

**yarn**

```bash
yarn add @rocicorp/zero zod

# Note: Modern Yarn doesn't run postinstall scripts by default.
# Add to package.json, then rebuild the native packages:
# "dependenciesMeta": {
#   "@rocicorp/zero-sqlite3": {
#     "built": true
#   }
# }
yarn rebuild @rocicorp/zero-sqlite3
```

These examples use Zod; any [Standard Schema](https://standardschema.dev/)-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.

**Drizzle**

**npm**

```bash
npm install -D drizzle-zero
npx drizzle-zero generate --output src/zero/schema.ts
```

**pnpm**

```bash
pnpm add -D drizzle-zero
pnpm exec drizzle-zero generate --output src/zero/schema.ts
```

**bun**

```bash
bun add -D drizzle-zero
bunx drizzle-zero generate --output src/zero/schema.ts
```

**yarn**

```bash
yarn add -D drizzle-zero
yarn exec drizzle-zero generate --output src/zero/schema.ts
```

**Prisma**

**npm**

```bash
npm install -D prisma-zero
# Add this to prisma/schema.prisma:
# generator zero {
#   provider = "prisma-zero"
#   output   = "../src/zero"
# }
npx prisma generate
```

**pnpm**

```bash
pnpm add -D prisma-zero
# Add this to prisma/schema.prisma:
# generator zero {
#   provider = "prisma-zero"
#   output   = "../src/zero"
# }
pnpx prisma generate
```

**bun**

```bash
bun add -D prisma-zero
# Add this to prisma/schema.prisma:
# generator zero {
#   provider = "prisma-zero"
#   output   = "../src/zero"
# }
bunx prisma generate
```

**yarn**

```bash
yarn add -D prisma-zero
# Add this to prisma/schema.prisma:
# generator zero {
#   provider = "prisma-zero"
#   output   = "../src/zero"
# }
yarn prisma generate
```

**Manual**

```ts
// src/zero/schema.ts
import {
  boolean,
  createBuilder,
  createSchema,
  string,
  table
} from '@rocicorp/zero'

const user = table('user')
  .columns({
    id: string(),
    name: string(),
    active: boolean()
  })
  .primaryKey('id')

export const schema = createSchema({
  tables: [user]
})

export const zql = createBuilder(schema)

declare module '@rocicorp/zero' {
  interface DefaultTypes {
    schema: typeof schema
  }
}
```

### Set 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.

**TanStack Start**

```tsx
// 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>
  )
}
```

**Next.js**

```tsx
// src/app/providers.tsx
'use client'

import {ZeroProvider} from '@rocicorp/zero/react'
import type {ZeroOptions} from '@rocicorp/zero'
import type {ReactNode} from 'react'
import {schema} from '../zero/schema'

const opts: ZeroOptions = {
  cacheURL: 'http://localhost:4848',
  schema
}

export function Providers({
  children
}: {
  children: ReactNode
}) {
  return <ZeroProvider {...opts}>{children}</ZeroProvider>
}

// src/app/layout.tsx
import type {ReactNode} from 'react'
import {Providers} from './providers'

export default function RootLayout({
  children
}: {
  children: ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}
```

**SolidStart**

```tsx
// src/app.tsx
import {MetaProvider, Title} from '@solidjs/meta'
import {Router} from '@solidjs/router'
import {FileRoutes} from '@solidjs/start/router'
import {ZeroProvider} from '@rocicorp/zero/solid'
import type {ZeroOptions} from '@rocicorp/zero'
import {Suspense} from 'solid-js'
import {schema} from './zero/schema'

const opts: ZeroOptions = {
  cacheURL: 'http://localhost:4848',
  schema
}

export default function App() {
  return (
    <ZeroProvider {...opts}>
      <Router
        root={props => (
          <MetaProvider>
            <Title>Zero App</Title>
            <Suspense>{props.children}</Suspense>
          </MetaProvider>
        )}
      >
        <FileRoutes />
      </Router>
    </ZeroProvider>
  )
}
```

**TypeScript**

```tsx
// src/zero.ts
import {Zero} from '@rocicorp/zero'
import type {ZeroOptions} from '@rocicorp/zero'
import {schema} from './zero/schema'

const opts: ZeroOptions = {
  cacheURL: 'http://localhost:4848',
  schema
}

const zero = new Zero(opts)

export {zero}
```

## Sync Data

### Define Query

Shared reads are conventionally stored in `queries.ts`. Use `zql` from `schema.ts` to construct and return a ZQL query:

```tsx
// 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](https://zero.rocicorp.dev/docs/queries) 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.

**TanStack Start**

```ts
// 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)
      }
    }
  }
})
```

**Next.js**

```ts
// src/app/api/query/route.ts
import {handleQueryRequest} from '@rocicorp/zero/server'
import {mustGetQuery} from '@rocicorp/zero'
import {queries} from '../../../zero/queries'
import {schema} from '../../../zero/schema'

export async function POST(request: 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)
}
```

**SolidStart**

```ts
// src/routes/api/query.ts
import type {APIEvent} from '@solidjs/start/server'
import {handleQueryRequest} from '@rocicorp/zero/server'
import {mustGetQuery} from '@rocicorp/zero'
import {queries} from '../../zero/queries'
import {schema} from '../../zero/schema'

export async function POST(event: APIEvent) {
  const result = await handleQueryRequest({
    handler: (name, args) => {
      const query = mustGetQuery(queries, name)
      return query.fn({args})
    },
    schema,
    request: event.request,
    userID: null
  })

  return Response.json(result)
}
```

**Hono**

```ts
// src/api/app.ts
import {Hono} from 'hono'
import {handleQueryRequest} from '@rocicorp/zero/server'
import {mustGetQuery} from '@rocicorp/zero'
import {queries} from '../zero/queries'
import {schema} from '../zero/schema'

const app = new Hono()

app.post('/api/query', async c => {
  const result = await handleQueryRequest({
    handler: (name, args) => {
      const query = mustGetQuery(queries, name)
      return query.fn({args})
    },
    schema,
    request: c.req.raw,
    userID: null
  })

  return c.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:

**React**

```tsx
import {useQuery} from '@rocicorp/zero/react'
import {queries} from './zero/queries'

const [users] = useQuery(queries.allUsers())
```

**SolidJS**

```tsx
import {useQuery} from '@rocicorp/zero/solid'
import {queries} from './zero/queries'

const [users] = useQuery(() => queries.allUsers())
```

**TypeScript**

```tsx
import {zero} from './zero'
import {queries} from './zero/queries'

const users = await zero.run(queries.allUsers())
```

### More about Queries

* [Filters, sorting, relationships, preloading, and more](https://zero.rocicorp.dev/docs/queries)
* [Server-driven authentication](https://zero.rocicorp.dev/docs/auth)

## Mutate Data

### Define Mutators

Data is written in Zero apps using mutators. Similar to queries, shared writes usually live in `mutators.ts`:

```tsx
// 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](https://zero.rocicorp.dev/docs/mutators#writing-data) 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:

**TanStack Start**

```tsx {3,9}
// 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
}
```

**Next.js**

```tsx {3,9}
// src/app/providers.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
}
```

**SolidStart**

```tsx {3,9}
// src/app.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
}
```

**TypeScript**

```tsx {3,9}
// src/zero.ts
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.

**Drizzle**

```ts
// 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
  }
}
```

**Kysely**

```ts
// src/zero/db-provider.ts
import {Kysely, PostgresDialect} from 'kysely'
import {zeroKysely} from '@rocicorp/zero/server/adapters/kysely'
import {Pool} from 'pg'
import {schema} from './schema'
import type {Database} from '../kysely/database'

const connectionString = process.env.ZERO_UPSTREAM_DB
if (!connectionString) {
  throw new Error('ZERO_UPSTREAM_DB is not set')
}

const kysely = new Kysely<Database>({
  dialect: new PostgresDialect({
    pool: new Pool({
      connectionString
    })
  })
})

export const dbProvider = zeroKysely(schema, kysely)

// Register global types for mutators on the server
declare module '@rocicorp/zero' {
  interface DefaultTypes {
    dbProvider: typeof dbProvider
  }
}
```

**Prisma**

```ts
// src/zero/db-provider.ts
import {PrismaPg} from '@prisma/adapter-pg'
import {PrismaClient} from '@prisma/client'
import {zeroPrisma} from '@rocicorp/zero/server/adapters/prisma'
import {schema} from './schema'

const connectionString = process.env.ZERO_UPSTREAM_DB
if (!connectionString) {
  throw new Error('ZERO_UPSTREAM_DB is not set')
}

const prisma = new PrismaClient({
  adapter: new PrismaPg({
    connectionString
  })
})

export const dbProvider = zeroPrisma(schema, prisma)

// Register global types for mutators on the server
declare module '@rocicorp/zero' {
  interface DefaultTypes {
    dbProvider: typeof dbProvider
  }
}
```

**node-postgres**

```ts
// src/zero/db-provider.ts
import {zeroNodePg} from '@rocicorp/zero/server/adapters/pg'
import {Pool} from 'pg'
import {schema} from './schema'

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 dbProvider = zeroNodePg(schema, pool)

// Register global types for mutators on the server
declare module '@rocicorp/zero' {
  interface DefaultTypes {
    dbProvider: typeof dbProvider
  }
}
```

**postgres.js**

```ts
// src/zero/db-provider.ts
import {zeroPostgresJS} from '@rocicorp/zero/server/adapters/postgresjs'
import postgres from 'postgres'
import {schema} from './schema'

const connectionString = process.env.ZERO_UPSTREAM_DB
if (!connectionString) {
  throw new Error('ZERO_UPSTREAM_DB is not set')
}

const sql = postgres(connectionString)

export const dbProvider = zeroPostgresJS(schema, sql)

// 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:

**TanStack Start**

```ts
// 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)
      }
    }
  }
})
```

**Next.js**

```ts
// src/app/api/mutate/route.ts
import {handleMutateRequest} from '@rocicorp/zero/server'
import {mustGetMutator} from '@rocicorp/zero'
import {mutators} from '../../../zero/mutators'
import {dbProvider} from '../../../zero/db-provider'

export async function POST(request: 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)
}
```

**SolidStart**

```ts
// src/routes/api/mutate.ts
import type {APIEvent} from '@solidjs/start/server'
import {handleMutateRequest} from '@rocicorp/zero/server'
import {mustGetMutator} from '@rocicorp/zero'
import {mutators} from '../../zero/mutators'
import {dbProvider} from '../../zero/db-provider'

export async function POST(event: APIEvent) {
  const result = await handleMutateRequest({
    dbProvider,
    handler: transact =>
      transact((tx, name, args) => {
        const mutator = mustGetMutator(mutators, name)
        return mutator.fn({args, tx})
      }),
    request: event.request,
    userID: null
  })

  return Response.json(result)
}
```

**Hono**

```ts
// src/api/app.ts
import {handleMutateRequest} from '@rocicorp/zero/server'
import {mustGetMutator} from '@rocicorp/zero'
import {mutators} from '../zero/mutators'
import {dbProvider} from '../zero/db-provider'

app.post('/api/mutate', async c => {
  const result = await handleMutateRequest({
    dbProvider,
    handler: transact =>
      transact((tx, name, args) => {
        const mutator = mustGetMutator(mutators, name)
        return mutator.fn({args, tx})
      }),
    request: c.req.raw,
    userID: null
  })

  return c.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.

> 🔐 **Add auth if you need it**: These examples have only public queries and mutators, so they do not pass a context. In authenticated apps, you should validate auth in the request, derive context from the session, and pass the context to the mutate and query handlers. See [Authentication](https://zero.rocicorp.dev/docs/auth).

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`.

**npm**

```bash
ZERO_QUERY_URL="http://localhost:3000/api/query" \
  ZERO_MUTATE_URL="http://localhost:3000/api/mutate" \
  npx zero-cache-dev
```

**pnpm**

```bash
ZERO_QUERY_URL="http://localhost:3000/api/query" \
  ZERO_MUTATE_URL="http://localhost:3000/api/mutate" \
  pnpm exec zero-cache-dev
```

**bun**

```bash
ZERO_QUERY_URL="http://localhost:3000/api/query" \
  ZERO_MUTATE_URL="http://localhost:3000/api/mutate" \
  bunx zero-cache-dev
```

**yarn**

```bash
ZERO_QUERY_URL="http://localhost:3000/api/query" \
  ZERO_MUTATE_URL="http://localhost:3000/api/mutate" \
  yarn exec zero-cache-dev
```

### Invoke Mutators

You can call a mutator with `zero.mutate`:

**React**

```tsx
import {useZero} from '@rocicorp/zero/react'
import {mutators} from './zero/mutators'

const zero = useZero()

const onClick = () => {
  zero.mutate(mutators.activateUser({id: '1'}))
}
```

**SolidJS**

```tsx
import {useZero} from '@rocicorp/zero/solid'
import {mutators} from './zero/mutators'

const zero = useZero()

const onClick = () => {
  zero().mutate(mutators.activateUser({id: '1'}))
}
```

**TypeScript**

```tsx
import {zero} from './zero'
import {mutators} from './zero/mutators'

await 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.

### More about Mutators

* [CRUD, server-specific code, permissions, and more](https://zero.rocicorp.dev/docs/mutators)
* [Server-driven auth and Context](https://zero.rocicorp.dev/docs/auth)
* [Learn how to deploy your app to production](https://zero.rocicorp.dev/docs/self-host)

**For AI agents**: to view all the available documentation, visit https://zero.rocicorp.dev/llms.txt