---
title: Manual Installation
description: Set up SurfSense from source, component by component
icon: Wrench
---

This guide sets up SurfSense without Docker (except for zero-cache, which is simplest to run as a container). Choose this path if you want to contribute to SurfSense or need full control over each component. If you just want to run SurfSense, the [Docker installation](/docs/docker-installation) is much faster.

## What You'll Need

- **Python 3.12+** — backend runtime
- **Node.js 20+** and **pnpm** — frontend runtime
- **PostgreSQL 14+** with the **pgvector** extension — database
- **Redis** — message broker for background tasks
- **Docker** — to run zero-cache (the real-time sync server)
- **Git** — to clone the repository
- **uv** — Python package manager ([install instructions](https://docs.astral.sh/uv/getting-started/installation/))

Clone the repository first:

```bash
git clone https://github.com/MODSetter/SurfSense.git
cd SurfSense
```

## Decisions to Make Up Front

**Authentication.** SurfSense supports local email/password auth (the default, no setup needed) or Google OAuth login. For Google login you need an OAuth client from the [Google Cloud Console](https://console.cloud.google.com/apis/credentials) — the [Google connectors guide](/docs/connectors/external/google) walks through creating one.

**Document parsing (ETL).** SurfSense converts uploaded files with one of three services:

- **Docling** (default) — runs locally, no API key, privacy-friendly. Supports PDF, Office docs, images, HTML, CSV.
- **Unstructured** — needs an API key from [Unstructured Platform](https://platform.unstructured.io/). Supports 34+ formats.
- **LlamaCloud** — needs an API key from [LlamaCloud](https://cloud.llamaindex.ai/). Supports 50+ formats.

You only need one. Docling is the easiest way to start.

## Backend Setup

### 1. Configure the Environment

Copy the example environment file:

**Linux/macOS:**

```bash
cd surfsense_backend
cp .env.example .env
```

**Windows (PowerShell):**

```powershell
cd surfsense_backend
Copy-Item -Path .env.example -Destination .env
```

`.env.example` is the source of truth for configuration — every variable is documented inline with comments and sensible defaults. At minimum, set your PostgreSQL connection string and a JWT secret key (generate one with `openssl rand -base64 32`). Everything else — auth type, ETL service, embeddings, TTS/STT, connector credentials — is optional and explained in the file itself.

### 2. Install Dependencies

```bash
# From surfsense_backend/
uv sync
```

### 3. Configure PostgreSQL for Zero Sync

SurfSense uses [Rocicorp Zero](https://zero.rocicorp.dev/) for real-time updates (notifications, document status, chat comments, indexing progress). Zero replicates data from PostgreSQL via **logical replication**, which requires a one-time PostgreSQL configuration change.

Edit your `postgresql.conf` (typical locations: `/etc/postgresql/<version>/main/postgresql.conf` on Linux, `/usr/local/var/postgres/postgresql.conf` on macOS via Homebrew, `C:\Program Files\PostgreSQL\<version>\data\postgresql.conf` on Windows) and set:

```ini
wal_level = logical
max_replication_slots = 10
max_wal_senders = 10
```

Then restart PostgreSQL:

**Linux:**

```bash
sudo systemctl restart postgresql
```

**macOS (Homebrew):**

```bash
brew services restart postgresql
```

**Windows (PowerShell, replace `17` with your major version):**

```powershell
Restart-Service postgresql-x64-17
```

Verify the change:

```bash
psql -U postgres -d surfsense -c "SHOW wal_level;"
# Should return: logical
```

**Managed databases (RDS, Supabase, Cloud SQL, etc.):** enable logical replication via your provider's parameter group (e.g. `rds.logical_replication=1` on RDS) and grant your database user the `REPLICATION` privilege:

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

### 4. Run Database Migrations

This creates the schema **and** the `zero_publication` that zero-cache needs to start. Skipping this step will cause zero-cache to crash-loop with `Unknown or invalid publications. Specified: [zero_publication]`.

```bash
# From surfsense_backend/
uv run alembic upgrade head
```

Verify the publication was created:

```bash
psql -U postgres -d surfsense -c "SELECT pubname FROM pg_publication;"
# Should include: zero_publication
```

### 5. Start Redis

**Linux:**

```bash
sudo systemctl start redis
# or run directly
redis-server
```

**macOS (Homebrew):**

```bash
brew services start redis
```

**Windows — run Redis in Docker (easiest):**

```powershell
docker run -d --name redis -p 6379:6379 redis:latest
```

Verify it's running:

```bash
redis-cli ping
# Should return: PONG
```

### 6. Start the Celery Worker

In a new terminal, start the worker that handles background tasks (document indexing, connector syncs):

**Linux/macOS:**

```bash
cd surfsense_backend
DEFAULT_Q="${CELERY_TASK_DEFAULT_QUEUE:-surfsense}"
uv run celery -A celery_worker.celery_app worker --loglevel=info --concurrency=1 --pool=solo --queues="${DEFAULT_Q},${DEFAULT_Q}.connectors,${DEFAULT_Q}.gateway"
```

**Windows (PowerShell):**

```powershell
cd surfsense_backend
uv run celery -A celery_worker.celery_app worker --loglevel=info --concurrency=1 --pool=solo --queues="surfsense,surfsense.connectors,surfsense.gateway"
```

Optionally, run [Flower](https://flower.readthedocs.io/) in another terminal to monitor tasks at [http://localhost:5555](http://localhost:5555):

```bash
uv run celery -A celery_worker.celery_app flower --port=5555
```

### 7. Start Celery Beat (Scheduler)

In another terminal, start the scheduler that triggers periodic tasks (like scheduled connector syncs). Without it, scheduled tasks won't run.

```bash
cd surfsense_backend
uv run celery -A celery_worker.celery_app beat --loglevel=info
```

### 8. Run the Backend

```bash
# From surfsense_backend/
uv run main.py

# Or with hot reloading for development
uv run main.py --reload
```

You should see the server running on `http://localhost:8000`.

## Zero-Cache Setup

**zero-cache** is the Rocicorp Zero server that sits between PostgreSQL and the browser. It streams real-time updates to all connected clients via WebSocket. Without it, the UI sits on stale data. For an overview of how Zero works, see the [Real-Time Sync with Zero](/docs/how-to/zero-sync) guide.

### 1. Run Zero-Cache via Docker

**Linux/macOS:**

```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_NUM_SYNC_WORKERS=4 \
  -e ZERO_UPSTREAM_MAX_CONNS=20 \
  -e ZERO_CVR_MAX_CONNS=30 \
  -e ZERO_QUERY_URL="http://host.docker.internal:3000/api/zero/query" \
  -e ZERO_MUTATE_URL="http://host.docker.internal:3000/api/zero/mutate" \
  -e ZERO_QUERY_FORWARD_COOKIES=true \
  -v surfsense-zero-cache:/data \
  rocicorp/zero:1.6.0
```

**Windows (PowerShell):**

```powershell
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_NUM_SYNC_WORKERS=4 `
  -e ZERO_UPSTREAM_MAX_CONNS=20 `
  -e ZERO_CVR_MAX_CONNS=30 `
  -e ZERO_QUERY_URL="http://host.docker.internal:3000/api/zero/query" `
  -e ZERO_MUTATE_URL="http://host.docker.internal:3000/api/zero/mutate" `
  -e ZERO_QUERY_FORWARD_COOKIES=true `
  -v surfsense-zero-cache:/data `
  rocicorp/zero:1.6.0
```

**Adjustments for your setup:**

- Replace `postgres:postgres` in the connection URLs with your actual database user and password.
- On Linux without Docker Desktop, `host.docker.internal` may not resolve. Either keep the `--add-host=host.docker.internal:host-gateway` flag (Docker 20.10+) or replace `host.docker.internal` with your host's IP / `--network=host` + `localhost`.
- For production / custom domains, set `ZERO_QUERY_URL` and `ZERO_MUTATE_URL` to your public frontend URL (e.g. `https://app.yourdomain.com/api/zero/query`).

### 2. Verify Zero-Cache

```bash
curl http://localhost:4848/keepalive
# Should return HTTP 200

# Tail logs to confirm initial replication completed without errors
docker logs -f surfsense-zero-cache
```

### Alternative: Let Docker Manage All Dependencies

If you'd rather have Docker manage Postgres, Redis, and zero-cache together (while still running the backend and frontend natively), the repository ships a deps-only compose file. **Run alembic migrations on the host first** so `zero_publication` exists before zero-cache starts:

```bash
cd surfsense_backend
uv run alembic upgrade head
cd ../docker
docker compose -f docker-compose.deps-only.yml up -d
```

The deps-only stack exposes zero-cache on port `4848`. Point the frontend at it by setting the zero-cache URL in `surfsense_web/.env` (see the comments in `surfsense_web/.env.example`).

## Frontend Setup

### 1. Configure the Environment

**Linux/macOS:**

```bash
cd surfsense_web
cp .env.example .env
```

**Windows (PowerShell):**

```powershell
cd surfsense_web
Copy-Item -Path .env.example -Destination .env
```

As with the backend, `.env.example` documents every option inline. Make sure the auth type and ETL service match what you configured for the backend, and that the backend URL points at `http://localhost:8000`.

### 2. Install Dependencies and Run

```bash
# Install pnpm if you don't have it
npm install -g pnpm

pnpm install
pnpm run dev
```

The frontend should now be running at [http://localhost:3000](http://localhost:3000).

## Browser Extension (Optional)

The SurfSense browser extension saves any webpage — including those behind authentication — straight into your knowledge base.

```bash
cd surfsense_browser_extension
cp .env.example .env   # set the backend URL, documented inline

pnpm install
pnpm build                     # Chrome (default)
pnpm build --target=firefox    # or Firefox
pnpm build --target=edge       # or Edge
```

Load the built extension in your browser's developer mode and configure it with your SurfSense API key. See the [Plasmo build docs](https://docs.plasmo.com/framework/workflows/build#with-a-specific-target) for details.

## Verify Your Installation

1. Open [http://localhost:3000](http://localhost:3000) and sign in.
2. Create a workspace and upload a document.
3. Watch the upload status update live without refreshing — this confirms zero-cache is wired up correctly.
4. Chat with your uploaded content.

## Troubleshooting

- **Database connection issues**: Verify PostgreSQL is running and pgvector is installed.
- **Redis connection issues**: `redis-cli ping` should return `PONG`. Check the Redis URL in your backend `.env`.
- **Celery worker issues**: Make sure the worker is running in a separate terminal and check its logs.
- **File upload failures**: Validate your ETL service API key, or use Docling which needs none.
- **Real-time updates not working / stale UI**: Verify zero-cache is running (`curl http://localhost:4848/keepalive` returns 200). Open browser DevTools → Console and look for WebSocket errors. Confirm the zero-cache URL in `surfsense_web/.env` matches the running zero-cache address.
- **Zero-cache stuck on `Unknown or invalid publications. Specified: [zero_publication]`**: You skipped `uv run alembic upgrade head`. Run it from `surfsense_backend/`, then `docker restart surfsense-zero-cache`.
- **Zero-cache crashes with `_zero.tableMetadata` errors**: A previous run left a half-built SQLite replica behind. Start fresh: `docker rm -f surfsense-zero-cache && docker volume rm surfsense-zero-cache`, then re-run the command from [Zero-Cache Setup](#zero-cache-setup).
- **`wal_level` is not `logical`**: zero-cache requires logical replication. Set it in `postgresql.conf`, restart PostgreSQL, and verify with `SHOW wal_level;`.
- **Backend `/ready` returns 503**: The readiness probe verifies `zero_publication` exists. Run `uv run alembic upgrade head` to create it.

## Next Steps

- Set up [connectors](/docs/connectors) to bring in your tools and services.
- Connect [local models](/docs/local-models) like Ollama or LM Studio.
- For production, put a reverse proxy in front, add SSL, and set up database backups.
