Skip to main content

Overview

The Zero client library (@rocicorp/zero) provides the core functionality for building real-time, sync-enabled applications. Built on top of Replicache, Zero provides a powerful client-side data layer with automatic sync, optimistic updates, and reactive queries.

Installation

npm install @rocicorp/zero

Creating a Zero Client

The Zero class is the main entry point for the client library. It manages the connection to your Zero server, handles sync, and provides query and mutation APIs.

Basic Setup

import { Zero, createSchema, table, string } from '@rocicorp/zero';

const schema = createSchema({
  tables: [
    table('user')
      .columns({
        id: string(),
        name: string(),
        email: string(),
      })
      .primaryKey('id'),
  ],
});

const zero = new Zero({
  schema,
  server: 'https://your-zero-server.com',
  userID: 'user-123',
});

Configuration Options

The Zero constructor accepts a ZeroOptions object:
OptionTypeRequiredDescription
schemaSchemaYesSchema definition for your data model
server or cacheURLstringYesURL of your Zero cache server
userIDstringYesUnique identifier for the current user
authstring | () => Promise<string>NoAuthentication token or function returning token
mutatorsCustomMutatorDefsNoCustom mutation functions
contextobjectNoUser context passed to mutations
logLevel'debug' | 'info' | 'error'NoLogging verbosity (default: ‘error’)
kvStore'idb' | 'mem' | KVStoreNoStorage backend (default: ‘idb’ for IndexedDB)
storageKeystringNoCustom storage key for multiple instances
mutateURLstringNoCustom endpoint for mutations
queryURLstringNoCustom endpoint for custom queries
pingTimeoutMsnumberNoPing timeout in milliseconds (default: 5000)
hiddenTabDisconnectDelaynumberNoTime before disconnecting hidden tabs (default: 300000)
onOnlineChange(online: boolean) => voidNoCallback when online status changes
onUpdateNeeded(reason: UpdateNeededReason) => voidNoCallback when client needs to update

With Authentication

const zero = new Zero({
  schema,
  server: 'https://your-zero-server.com',
  userID: 'user-123',
  auth: 'your-auth-token',
  // Or as a function for dynamic tokens
  auth: async () => {
    const response = await fetch('/api/auth/token');
    return response.text();
  },
});

Connection Management

Zero automatically manages the WebSocket connection to your server.

Connection State

// Get current connection state
const state = zero.connection.state.current;

// Subscribe to connection state changes
const unsubscribe = zero.connection.state.subscribe((newState) => {
  console.log('Connection status:', newState.status);
  console.log('Connected:', newState.connected);
});

// Clean up subscription
unsubscribe();

Connection Status Values

  • Connecting - Attempting to establish connection
  • Connected - Successfully connected and syncing
  • Error - Connection error occurred
  • Disconnected - Intentionally disconnected

Manual Connection Control

// Manually trigger connection
await zero.connection.connect();

// Manually trigger connection with new auth
await zero.connection.connect({ auth: 'new-token' });

// Disconnect
await zero.connection.disconnect();

Online Status

// Check if client is online
if (zero.online) {
  console.log('Client is connected and syncing');
}

// Subscribe to online/offline changes
const zero = new Zero({
  schema,
  server: 'https://your-zero-server.com',
  userID: 'user-123',
  onOnlineChange: (online) => {
    if (online) {
      console.log('Client is now online');
    } else {
      console.log('Client is now offline');
    }
  },
});

Query Builder

Zero provides a powerful query builder API for querying data from your schema.
import { createBuilder } from '@rocicorp/zero';

const zql = createBuilder(schema);

// Simple query
const allUsers = zql.user;

// Filter by condition
const activeUsers = zql.user.where('status', 'active');

// Get a single record
const user = zql.user.where('id', 'user-123').one();

// Complex queries with multiple conditions
const filteredUsers = zql.user
  .where('status', 'active')
  .where('role', 'admin')
  .orderBy('name', 'asc')
  .limit(10);
See Queries for detailed query API documentation.

Materializing Queries

Materialize a query to get reactive updates:
const view = zero.materialize(zql.user.where('status', 'active'));

// Listen for changes
view.addListener((data, resultType) => {
  console.log('Users:', data);
  console.log('Result type:', resultType); // 'unknown', 'complete', or 'error'
});

// Clean up
view.destroy();

Materialization Options

const view = zero.materialize(query, {
  ttl: '5m', // Keep alive for 5 minutes after last subscriber
  // ttl can be: 'forever', 'never', '30s', '5m', or milliseconds
});

Mutations

Zero provides multiple ways to mutate data:

CRUD Operations (Legacy)

// Insert
await zero.mutate.user.insert({
  id: 'user-123',
  name: 'Alice',
  email: '[email protected]',
});

// Update
await zero.mutate.user.update({
  id: 'user-123',
  name: 'Alice Smith',
});

// Upsert
await zero.mutate.user.upsert({
  id: 'user-123',
  name: 'Alice',
  email: '[email protected]',
});

// Delete
await zero.mutate.user.delete('user-123');

Batch Mutations

await zero.mutateBatch(async (m) => {
  await m.user.insert({ id: 'user-1', name: 'Alice' });
  await m.user.insert({ id: 'user-2', name: 'Bob' });
  await m.post.insert({ id: 'post-1', title: 'Hello' });
});

Custom Mutations

Define custom business logic with mutation functions:
import { defineMutators } from '@rocicorp/zero';

const mutators = defineMutators(schema, {
  createUserWithProfile: async (tx, args: { name: string; bio: string }) => {
    const userID = generateID();
    await tx.mutate.user.insert({ id: userID, name: args.name });
    await tx.mutate.profile.insert({ userID, bio: args.bio });
  },
});

const zero = new Zero({
  schema,
  server: 'https://your-zero-server.com',
  userID: 'user-123',
  mutators,
});

// Use custom mutator
await zero.mutate.createUserWithProfile({
  name: 'Alice',
  bio: 'Software engineer',
});
See Mutations for detailed mutation API documentation.

Client Information

// Get client ID
console.log(zero.clientID); // Unique ID for this client instance

// Get user ID
console.log(zero.userID); // User ID passed in options

// Get version
console.log(zero.version); // Zero client version

Cleanup

Always close the Zero client when you’re done:
// Close the client and clean up resources
await zero.close();

TypeScript Types

Zero is fully typed with TypeScript. Define default types for better type inference:
// Define default types module augmentation
declare module '@rocicorp/zero' {
  interface DefaultTypes {
    schema: typeof schema;
    context: { sub: string; role: string };
  }
}

// Now hooks and queries are fully typed
const [users] = useQuery(zql.user);
// users is typed as User[]

Storage Options

Zero supports multiple storage backends:

IndexedDB (Default)

const zero = new Zero({
  schema,
  server: 'https://your-zero-server.com',
  userID: 'user-123',
  kvStore: 'idb', // Default
});

In-Memory (Testing)

const zero = new Zero({
  schema,
  server: 'https://your-zero-server.com',
  userID: 'user-123',
  kvStore: 'mem', // No persistence
});

Custom Storage

import { createCustomKVStore } from './my-storage';

const zero = new Zero({
  schema,
  server: 'https://your-zero-server.com',
  userID: 'user-123',
  kvStore: createCustomKVStore(),
});

Error Handling

try {
  await zero.mutate.user.insert({ id: 'user-123', name: 'Alice' });
} catch (error) {
  if (error instanceof ApplicationError) {
    // Application-level error from server
    console.error('Application error:', error.message);
  } else {
    // Network or other error
    console.error('Error:', error);
  }
}

Best Practices

Single Instance: Create one Zero instance per user session and reuse it throughout your application.
Clean Up Views: Always call view.destroy() when you’re done with materialized queries to prevent memory leaks.
Use Framework Hooks: For React, Solid, and React Native, use the framework-specific hooks instead of calling materialize directly.
Don’t Block Renders: Avoid creating Zero instances or materializing queries during component render. Use effects or providers instead.

Next Steps

Queries

Learn the query API for fetching and filtering data

Mutations

Explore mutation patterns for data changes

React Integration

Use Zero with React hooks

Solid Integration

Use Zero with Solid.js

Build docs developers (and LLMs) love