---
title: Real-Time Sync with Zero
description: How SurfSense uses Rocicorp Zero for instant real-time data synchronization
---

# Real-Time Sync with Zero

SurfSense uses [Rocicorp Zero](https://zero.rocicorp.dev/) for real-time data synchronization. Zero continuously replicates data from PostgreSQL to a local cache on each client, enabling instant UI updates for notifications, documents, connectors, chat messages, and comments.

## How It Works

Zero runs a **zero-cache** server that sits between PostgreSQL and the browser:

1. **zero-cache** replicates data from PostgreSQL into a local SQLite replica using logical replication
2. The browser connects to zero-cache via WebSocket and syncs relevant data locally
3. When data changes in PostgreSQL (e.g., a new notification), zero-cache pushes the update to all connected clients instantly
4. Queries run against local data first for instant results, then update when server data arrives

## Architecture

| Component | Role |
|-----------|------|
| **PostgreSQL** | Source of truth (with `wal_level=logical`) |
| **zero-cache** | Replicates Postgres → SQLite, serves client sync via WebSocket |
| **Browser** | Stores synced data locally, runs queries against local cache |

## Configuration

### Docker Deployment

zero-cache is included in the Docker Compose setup. The key environment variables are:

| Variable | Description | Default |
|----------|-------------|---------|
| `SURFSENSE_PUBLIC_URL` | Public SurfSense origin used by the browser | `http://localhost:3929` |
| `ZERO_ADMIN_PASSWORD` | Password for the zero-cache admin UI and `/statz` endpoint | `surfsense-zero-admin` |
| `ZERO_UPSTREAM_DB` | PostgreSQL connection URL for replication | Built from `DB_*` vars |
| `/zero` | Same-origin browser path Caddy routes to zero-cache | `${SURFSENSE_PUBLIC_URL}/zero` |
| `ZERO_APP_PUBLICATIONS` | PostgreSQL publication restricting which tables are replicated | `zero_publication` |
| `ZERO_NUM_SYNC_WORKERS` | Number of view-sync worker processes. Must be ≤ `ZERO_UPSTREAM_MAX_CONNS` and ≤ `ZERO_CVR_MAX_CONNS` | `4` |
| `ZERO_UPSTREAM_MAX_CONNS` | Max connections to upstream PostgreSQL for mutations | `20` |
| `ZERO_CVR_MAX_CONNS` | Max connections to the CVR database | `30` |

### Manual / Local Development

If running the frontend outside Docker (e.g. `pnpm dev`), you need:

1. **A running zero-cache instance** pointing at your PostgreSQL database. The easiest path is the official Docker image:

    ```bash
    docker run -d --name surfsense-zero-cache \
      -p 4848:4848 \
      --add-host=host.docker.internal:host-gateway \
      -e ZERO_UPSTREAM_DB="postgresql://postgres:postgres@host.docker.internal:5432/surfsense?sslmode=disable" \
      -e ZERO_CVR_DB="postgresql://postgres:postgres@host.docker.internal:5432/surfsense?sslmode=disable" \
      -e ZERO_CHANGE_DB="postgresql://postgres:postgres@host.docker.internal:5432/surfsense?sslmode=disable" \
      -e ZERO_REPLICA_FILE=/data/zero.db \
      -e ZERO_ADMIN_PASSWORD=surfsense-zero-admin \
      -e ZERO_APP_PUBLICATIONS=zero_publication \
      -e ZERO_QUERY_URL="http://host.docker.internal:3000/api/zero/query" \
      -e ZERO_MUTATE_URL="http://host.docker.internal:3000/api/zero/mutate" \
      -v surfsense-zero-cache:/data \
      rocicorp/zero:1.4.0
    ```

    Run `uv run alembic upgrade head` from `surfsense_backend/` **before** starting this container so the `zero_publication` exists.
2. If the frontend is not behind bundled Caddy, set `NEXT_PUBLIC_ZERO_CACHE_URL=http://localhost:4848` before building/running the frontend so the browser connects directly to zero-cache.
3. **`wal_level = logical`** in your PostgreSQL config (see [Manual Installation → Configure PostgreSQL for Zero Sync](/docs/manual-installation#3-configure-postgresql-for-zero-sync)).

For the full manual setup walkthrough, see the [Manual Installation guide](/docs/manual-installation).

### Custom Domain / Reverse Proxy

The production Docker stack includes Caddy by default. Zero is exposed under the
same public origin as the app at `${SURFSENSE_PUBLIC_URL}/zero`, for example
`https://surf.example.com/zero`. Zero accepts this single path-component base
URL, so Caddy forwards `/zero/*` to the internal `zero-cache:4848` service
without stripping the prefix.

### Database Requirements

zero-cache connects to PostgreSQL using logical replication. The database must meet these requirements:

1. **`wal_level = logical`** — already configured in the bundled `postgresql.conf`
2. **The database user must have `REPLICATION` privilege** — required for creating logical replication slots

In the default Docker setup, the `surfsense` user is a PostgreSQL superuser and has all required privileges automatically.

**For managed databases** (RDS, Supabase, Cloud SQL, etc.) where the app user may not be a superuser, you need to grant replication privileges:

```sql
ALTER USER surfsense WITH REPLICATION;
GRANT CREATE ON DATABASE surfsense TO surfsense;
```

The `REPLICATION` privilege allows zero-cache to create a logical replication slot for streaming changes. The `CREATE` privilege allows zero-cache to create internal schemas (`zero`, `zero_0`) for its metadata.

## Synced Tables

Zero syncs the following tables for real-time features:

| Table | Used By |
|-------|---------|
| `notifications` | Inbox (comments, document processing, connector status) |
| `documents` | Document list, processing status indicators |
| `folders` | Nested folder tree for organizing documents |
| `search_source_connectors` | Connector status, indexing progress |
| `new_chat_messages` | Live chat message sync for shared chats |
| `chat_comments` | Real-time comment threads on AI responses |
| `chat_session_state` | Collaboration indicators (who is typing) |
| `automation_runs` | Live run status and per-step progress (thin column set; heavy fields stay on REST) |

## Troubleshooting

- **zero-cache not starting**: Check `docker compose logs zero-cache`. Ensure PostgreSQL has `wal_level=logical` (configured in `postgresql.conf`).
- **"Insufficient upstream connections" error**: zero-cache defaults `ZERO_NUM_SYNC_WORKERS` to the number of CPU cores, which can exceed connection pool limits on high-core machines. Lower `ZERO_NUM_SYNC_WORKERS` or raise `ZERO_UPSTREAM_MAX_CONNS` / `ZERO_CVR_MAX_CONNS` in your `.env`.
- **Frontend not syncing**: Open DevTools → Console and check for WebSocket connection errors. In production Docker, verify Caddy serves `${SURFSENSE_PUBLIC_URL}/zero`. In manual local development, verify `NEXT_PUBLIC_ZERO_CACHE_URL` points at the running zero-cache port.
- **Stale data after restart**: zero-cache rebuilds its SQLite replica from PostgreSQL on startup. This may take a moment for large databases.

## Learn More

- [Rocicorp Zero Documentation](https://zero.rocicorp.dev/docs)
- [Zero Schema Reference](https://zero.rocicorp.dev/docs/schema)
- [Zero Deployment Guide](https://zero.rocicorp.dev/docs/deployment)
