Manual Installation
Set up SurfSense from source, component by component
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 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)
Clone the repository first:
git clone https://github.com/MODSetter/SurfSense.git
cd SurfSenseDecisions 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 — the Google connectors guide 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. Supports 34+ formats.
- LlamaCloud — needs an API key from LlamaCloud. 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:
cd surfsense_backend
cp .env.example .envWindows (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
# From surfsense_backend/
uv sync3. Configure PostgreSQL for Zero Sync
SurfSense uses Rocicorp Zero 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:
wal_level = logical
max_replication_slots = 10
max_wal_senders = 10Then restart PostgreSQL:
Linux:
sudo systemctl restart postgresqlmacOS (Homebrew):
brew services restart postgresqlWindows (PowerShell, replace 17 with your major version):
Restart-Service postgresql-x64-17Verify the change:
psql -U postgres -d surfsense -c "SHOW wal_level;"
# Should return: logicalManaged 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:
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].
# From surfsense_backend/
uv run alembic upgrade headVerify the publication was created:
psql -U postgres -d surfsense -c "SELECT pubname FROM pg_publication;"
# Should include: zero_publication5. Start Redis
Linux:
sudo systemctl start redis
# or run directly
redis-servermacOS (Homebrew):
brew services start redisWindows — run Redis in Docker (easiest):
docker run -d --name redis -p 6379:6379 redis:latestVerify it's running:
redis-cli ping
# Should return: PONG6. Start the Celery Worker
In a new terminal, start the worker that handles background tasks (document indexing, connector syncs):
Linux/macOS:
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):
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 in another terminal to monitor tasks at http://localhost:5555:
uv run celery -A celery_worker.celery_app flower --port=55557. 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.
cd surfsense_backend
uv run celery -A celery_worker.celery_app beat --loglevel=info8. Run the Backend
# From surfsense_backend/
uv run main.py
# Or with hot reloading for development
uv run main.py --reloadYou 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 guide.
1. Run Zero-Cache via Docker
Linux/macOS:
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.0Windows (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.0Adjustments for your setup:
- Replace
postgres:postgresin the connection URLs with your actual database user and password. - On Linux without Docker Desktop,
host.docker.internalmay not resolve. Either keep the--add-host=host.docker.internal:host-gatewayflag (Docker 20.10+) or replacehost.docker.internalwith your host's IP /--network=host+localhost. - For production / custom domains, set
ZERO_QUERY_URLandZERO_MUTATE_URLto your public frontend URL (e.g.https://app.yourdomain.com/api/zero/query).
2. Verify Zero-Cache
curl http://localhost:4848/keepalive
# Should return HTTP 200
# Tail logs to confirm initial replication completed without errors
docker logs -f surfsense-zero-cacheAlternative: 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:
cd surfsense_backend
uv run alembic upgrade head
cd ../docker
docker compose -f docker-compose.deps-only.yml up -dThe 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:
cd surfsense_web
cp .env.example .envWindows (PowerShell):
cd surfsense_web
Copy-Item -Path .env.example -Destination .envAs 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
# Install pnpm if you don't have it
npm install -g pnpm
pnpm install
pnpm run devThe frontend should now be running at http://localhost:3000.
Browser Extension (Optional)
The SurfSense browser extension saves any webpage — including those behind authentication — straight into your knowledge base.
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 EdgeLoad the built extension in your browser's developer mode and configure it with your SurfSense API key. See the Plasmo build docs for details.
Verify Your Installation
- Open http://localhost:3000 and sign in.
- Create a workspace and upload a document.
- Watch the upload status update live without refreshing — this confirms zero-cache is wired up correctly.
- Chat with your uploaded content.
Troubleshooting
- Database connection issues: Verify PostgreSQL is running and pgvector is installed.
- Redis connection issues:
redis-cli pingshould returnPONG. 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/keepalivereturns 200). Open browser DevTools → Console and look for WebSocket errors. Confirm the zero-cache URL insurfsense_web/.envmatches the running zero-cache address. - Zero-cache stuck on
Unknown or invalid publications. Specified: [zero_publication]: You skippeduv run alembic upgrade head. Run it fromsurfsense_backend/, thendocker restart surfsense-zero-cache. - Zero-cache crashes with
_zero.tableMetadataerrors: 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. wal_levelis notlogical: zero-cache requires logical replication. Set it inpostgresql.conf, restart PostgreSQL, and verify withSHOW wal_level;.- Backend
/readyreturns 503: The readiness probe verifieszero_publicationexists. Runuv run alembic upgrade headto create it.
Next Steps
- Set up connectors to bring in your tools and services.
- Connect local models like Ollama or LM Studio.
- For production, put a reverse proxy in front, add SSL, and set up database backups.