Skip to main content
Technology & EngineeringLocal First455 lines

zero-sync

Teaches Zero (by Rocicorp), the successor to Replicache, a sync engine for building local-first web applications with instant UI, optimistic mutations, and server-side authority. Covers the Zero architecture (client cache, sync engine, server), defining queries and mutators, the reactivity model, server-side authorization and permissions, optimistic updates with automatic rollback, deployment patterns, and migration from Replicache.

Quick Summary24 lines
Zero is a sync engine that gives web apps instant, reactive UI with server-authoritative data and automatic sync.

## Key Points

- **Instant UI**: reads from local cache, never waits for the network
- **Optimistic mutations**: writes apply locally, then sync to server
- **Server authority**: server can reject mutations (auth, validation)
- **Reactive queries**: UI updates automatically when data changes
- **Offline support**: works without connectivity, syncs when reconnected
1. User clicks "Delete" on a todo
2. Client mutator runs: todo disappears from UI instantly
3. Mutation sent to server
4. Server checks: user doesn't own this todo → REJECT
5. Client receives rejection
6. Optimistic update is rolled back: todo reappears in UI
7. (Optional) Error callback notifies the user

## Quick Example

```bash
npm install @rocicorp/zero
```
skilldb get local-first-skills/zero-syncFull skill: 455 lines
Paste into your CLAUDE.md or agent config

Zero (Rocicorp)

Zero is a sync engine that gives web apps instant, reactive UI with server-authoritative data and automatic sync.


What Zero Is

Zero (from Rocicorp, creators of Replicache) is a client-side sync engine. It maintains a local cache of server data, applies mutations optimistically, and syncs with the server in the background. The server remains the authority — if it rejects a mutation, the client rolls back automatically.

┌──────────────────────┐        ┌──────────────────────┐
│       Client         │        │       Server         │
│                      │        │                      │
│  ┌────────────────┐  │        │  ┌────────────────┐  │
│  │  Zero Cache    │  │  sync  │  │   Postgres /   │  │
│  │  (SQLite/IDB)  │◄─┼───────┼──┤   Your DB      │  │
│  └───────┬────────┘  │        │  └────────────────┘  │
│          │           │        │                      │
│  ┌───────▼────────┐  │        │  ┌────────────────┐  │
│  │  Reactive      │  │        │  │  Mutator       │  │
│  │  Queries       │  │        │  │  Handlers      │  │
│  └───────┬────────┘  │        │  │  (authorize +  │  │
│          │           │        │  │   validate)    │  │
│  ┌───────▼────────┐  │        │  └────────────────┘  │
│  │  UI (React)    │  │        │                      │
│  └────────────────┘  │        │                      │
└──────────────────────┘        └──────────────────────┘

Key properties:

  • Instant UI: reads from local cache, never waits for the network
  • Optimistic mutations: writes apply locally, then sync to server
  • Server authority: server can reject mutations (auth, validation)
  • Reactive queries: UI updates automatically when data changes
  • Offline support: works without connectivity, syncs when reconnected

Setup

Install

npm install @rocicorp/zero

Define Your Schema

Zero uses a schema definition that mirrors your database structure.

// schema.ts
import { createSchema, defineTable, column } from '@rocicorp/zero';

const todoTable = defineTable('todo', {
  id: column.string(),
  title: column.string(),
  completed: column.boolean(),
  sortOrder: column.number(),
  createdAt: column.number(),
  ownerId: column.string(),
});

const userTable = defineTable('user', {
  id: column.string(),
  name: column.string(),
  email: column.string(),
});

export const schema = createSchema({
  tables: [todoTable, userTable],
});

export type Schema = typeof schema;

Initialize the Client

// zero-client.ts
import { Zero } from '@rocicorp/zero';
import { schema, type Schema } from './schema';

export const zero = new Zero<Schema>({
  server: 'https://my-zero-server.example.com',
  schema,
  userID: currentUser.id,
  auth: () => getAuthToken(),

  // Optional: enable local persistence
  kvStore: 'idb', // IndexedDB for persistence across page loads
});

Queries

Zero queries are reactive — they automatically update the UI when underlying data changes, whether from local mutations or remote sync.

import { useQuery } from '@rocicorp/zero/react';
import { zero } from './zero-client';

function TodoList() {
  // This query is reactive and instant (reads from local cache)
  const todos = useQuery(
    zero.query.todo
      .where('ownerId', '=', currentUser.id)
      .orderBy('sortOrder', 'asc')
  );

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>
          <input
            type="checkbox"
            checked={todo.completed}
            onChange={() => toggleTodo(todo.id, !todo.completed)}
          />
          {todo.title}
        </li>
      ))}
    </ul>
  );
}

Query API

// Basic where clause
const activeTodos = zero.query.todo
  .where('completed', '=', false);

// Multiple conditions
const myActiveTodos = zero.query.todo
  .where('ownerId', '=', userId)
  .where('completed', '=', false);

// Ordering and limiting
const recentTodos = zero.query.todo
  .orderBy('createdAt', 'desc')
  .limit(10);

// Related data (joins defined in schema)
const todosWithOwner = zero.query.todo
  .related('owner'); // returns todo with nested owner object

Mutators

Mutators define how data can be changed. They run optimistically on the client and authoritatively on the server.

Client-Side Mutators

// mutators.ts
import { type Schema } from './schema';
import { type Mutators } from '@rocicorp/zero';

export const mutators = {
  addTodo: (tx, { id, title, ownerId }: {
    id: string;
    title: string;
    ownerId: string;
  }) => {
    tx.set('todo', {
      id,
      title,
      completed: false,
      sortOrder: Date.now(),
      createdAt: Date.now(),
      ownerId,
    });
  },

  toggleTodo: (tx, { id, completed }: {
    id: string;
    completed: boolean;
  }) => {
    tx.update('todo', id, { completed });
  },

  deleteTodo: (tx, { id }: { id: string }) => {
    tx.delete('todo', id);
  },

  reorderTodo: (tx, { id, sortOrder }: {
    id: string;
    sortOrder: number;
  }) => {
    tx.update('todo', id, { sortOrder });
  },
} satisfies Mutators<Schema>;

Using Mutators in Components

function AddTodoForm() {
  const [title, setTitle] = useState('');

  function handleSubmit(e: FormEvent) {
    e.preventDefault();
    // This applies instantly to the local cache
    // Then syncs to the server in the background
    zero.mutate.addTodo({
      id: crypto.randomUUID(),
      title,
      ownerId: currentUser.id,
    });
    setTitle('');
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        placeholder="What needs to be done?"
      />
      <button type="submit">Add</button>
    </form>
  );
}

Server-Side Authorization

The server validates and authorizes every mutation. If rejected, the client automatically rolls back the optimistic update.

// server/mutators.ts
import { type ServerMutators } from '@rocicorp/zero/server';
import { type Schema } from '../schema';

export const serverMutators: ServerMutators<Schema> = {
  addTodo: async (tx, args, { userID }) => {
    // Authorization: user can only create their own todos
    if (args.ownerId !== userID) {
      throw new Error('Cannot create todos for other users');
    }

    // Validation
    if (!args.title || args.title.length > 500) {
      throw new Error('Title must be between 1 and 500 characters');
    }

    // Apply the mutation (same shape as client mutator)
    tx.set('todo', {
      ...args,
      completed: false,
      createdAt: Date.now(),
    });
  },

  toggleTodo: async (tx, args, { userID }) => {
    const todo = await tx.get('todo', args.id);
    if (!todo || todo.ownerId !== userID) {
      throw new Error('Not authorized');
    }
    tx.update('todo', args.id, { completed: args.completed });
  },

  deleteTodo: async (tx, args, { userID }) => {
    const todo = await tx.get('todo', args.id);
    if (!todo || todo.ownerId !== userID) {
      throw new Error('Not authorized');
    }
    tx.delete('todo', args.id);
  },
};

Rollback Flow

1. User clicks "Delete" on a todo
2. Client mutator runs: todo disappears from UI instantly
3. Mutation sent to server
4. Server checks: user doesn't own this todo → REJECT
5. Client receives rejection
6. Optimistic update is rolled back: todo reappears in UI
7. (Optional) Error callback notifies the user

Optimistic Updates with Conflict Handling

// Zero automatically handles the optimistic → confirmed flow
// But you can listen for rejections:

zero.onMutationRejected((mutationName, args, error) => {
  console.error(`Mutation ${mutationName} rejected:`, error);
  toast.error(`Action failed: ${error.message}`);
});

// Pending mutations indicator
function SyncStatus() {
  const pendingCount = useQuery(zero.pendingMutations);

  return (
    <div>
      {pendingCount > 0 && (
        <span>{pendingCount} changes syncing...</span>
      )}
    </div>
  );
}

Offline Behavior

Zero works offline by default when configured with local persistence.

const zero = new Zero<Schema>({
  server: 'https://my-server.com',
  schema,
  userID: currentUser.id,
  kvStore: 'idb', // enables offline persistence
});

// All reads work offline (from local cache)
// All mutations work offline (queued locally)
// When connectivity returns, mutations sync automatically

// Detect connectivity
function useConnectionStatus() {
  const [status, setStatus] = useState<'connected' | 'disconnected'>('connected');

  useEffect(() => {
    const unsubscribe = zero.onConnectionChange((connected) => {
      setStatus(connected ? 'connected' : 'disconnected');
    });
    return unsubscribe;
  }, []);

  return status;
}

Deployment

Server Setup

// server.ts
import { ZeroServer } from '@rocicorp/zero/server';
import { schema } from './schema';
import { serverMutators } from './server/mutators';

const server = new ZeroServer({
  schema,
  mutators: serverMutators,
  database: {
    type: 'postgres',
    connectionString: process.env.DATABASE_URL,
  },
  auth: {
    verifyToken: async (token) => {
      const payload = await verifyJWT(token);
      return { userID: payload.sub };
    },
  },
});

server.listen(3001);

Production Architecture

                    ┌─────────────┐
                    │   CDN       │
                    │ (static)    │
                    └──────┬──────┘
                           │
Users ──────►  ┌───────────▼───────────┐
               │   Load Balancer       │
               └───┬───────────┬───────┘
                   │           │
            ┌──────▼──┐  ┌────▼─────┐
            │ Zero    │  │ Zero     │
            │ Server 1│  │ Server 2 │
            └────┬────┘  └────┬─────┘
                 │            │
            ┌────▼────────────▼─────┐
            │      Postgres         │
            │  (primary + replicas) │
            └───────────────────────┘

Production Checklist

[ ] JWT-based authentication configured
[ ] Server mutators validate all inputs
[ ] Server mutators enforce authorization
[ ] Database connection pooling enabled
[ ] SSL/TLS on all connections
[ ] Rate limiting on mutation endpoints
[ ] Monitoring: sync latency, pending mutations, error rates
[ ] Load testing with expected concurrent users

Migration from Replicache

Zero is the successor to Replicache. Key differences:

ReplicacheZero
Pull/push modelContinuous sync stream
Manual query cachingBuilt-in reactive queries
Client-group basedUser-based sync
Custom backend integrationPostgres-native
Generic KV storeTyped schema with tables
// Replicache pattern
const rep = new Replicache({
  pushURL: '/api/push',
  pullURL: '/api/pull',
  mutators: { addTodo },
});
const todos = useSubscribe(rep, (tx) => tx.scan({ prefix: 'todo/' }).toArray());

// Zero pattern
const z = new Zero({ server: 'https://...', schema });
const todos = useQuery(z.query.todo.where('ownerId', '=', userId));

Zero eliminates the need to build custom push/pull endpoints. The server handles sync automatically with your Postgres database.

Install this skill directly: skilldb add local-first-skills

Get CLI access →

Related Skills

crdt-fundamentals

Teaches Conflict-free Replicated Data Types (CRDTs), the mathematical foundation for local-first sync. Covers how CRDTs guarantee eventual consistency without coordination, the difference between state-based and operation-based CRDTs, and practical implementations of G-Counter, PN-Counter, LWW-Register, OR-Set, G-Set, and RGA (Replicated Growable Array). Includes causal ordering, vector clocks, and guidance on choosing the right CRDT for your data model.

Local First454L

electric-sql

Teaches ElectricSQL, a Postgres-backed local-first sync framework. Covers the Electric architecture where Postgres is the source of truth and data syncs to local SQLite databases on client devices via shape-based partial replication. Includes shape definitions, live queries, offline-first patterns, conflict resolution with rich CRDTs, integration with React and Expo (React Native), deployment patterns, and migration strategies.

Local First433L

indexeddb-patterns

Teaches IndexedDB patterns for local-first web applications, using Dexie.js as the primary wrapper library. Covers schema design and versioning, creating indexes for efficient queries, transaction patterns, performance optimization (bulk operations, pagination, lazy loading), migration strategies for schema evolution, storage quota management, data export and import, and integration patterns with sync engines and reactive frameworks.

Local First556L

local-first-auth

Teaches authentication and authorization patterns for local-first applications that must work offline. Covers offline-capable auth with cached tokens, permission sync and local enforcement, encrypted local storage for sensitive data, key management with device-bound keys, device authorization and revocation, multi-device identity linking, end-to-end encryption for synced data, and secure patterns for handling auth in disconnected environments.

Local First606L

local-first-fundamentals

Teaches the local-first software paradigm where applications store data on the user's device, work fully offline, and sync to peers or servers when connectivity is available. Covers the spectrum from cloud-first to offline-first to local-first, core benefits (instant UX, offline capability, data ownership, privacy), key challenges (conflict resolution, sync complexity, storage limits), architectural patterns, and decision frameworks for when local-first is the right choice.

Local First285L

sync-engine-architecture

Teaches how to design and build a sync engine for local-first applications. Covers the operation log as the foundation, conflict resolution strategies (last-write-wins, operational transform, CRDTs), server reconciliation patterns, partial sync for large datasets, bandwidth optimization techniques, version vectors and causal consistency, clock synchronization, and practical implementation patterns with code examples.

Local First572L