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.
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 linesZero (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:
| Replicache | Zero |
|---|---|
| Pull/push model | Continuous sync stream |
| Manual query caching | Built-in reactive queries |
| Client-group based | User-based sync |
| Custom backend integration | Postgres-native |
| Generic KV store | Typed 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
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.
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.
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-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-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.
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.