Self-hosting
You can run your own backend and point the app at it. The server stores encrypted items and
holds no key that opens them, so self-hosting does not change the security model; you see the
same metadata any operator does (listed in the security model). This page is a provider-agnostic overview;
the full operational runbook, including one worked VPS setup, hardening, and the deploy
pipeline, is in the backend repo’s
docs/DEPLOY.md.
What you are running
Section titled “What you are running”Four parts. Only the first is code you ship; the other three are services it talks to.
| Component | Role |
|---|---|
| FastAPI service | The API and the /ws realtime endpoint. Stateless; run one or many replicas. |
| Redis | WebSocket pub/sub fan-out and device presence. Required. Holds only ephemeral state. |
| Postgres + Auth | Ciphertext store and identity. Supabase provides both; the backend verifies its tokens and never signs one. |
| Blob storage | Encrypted image and file blobs, via presigned URLs. Any S3-compatible store (Cloudflare R2, MinIO, AWS S3). |
Redis is not optional: realtime and presence depend on it. Text and note sync work without blob storage; only large image and file attachments need it.
<<compiled-in URLs>>
Desktop app
<<stateless>>
FastAPI service
<<Supabase, ciphertext>>
Postgres
<<issues tokens>>
Supabase Auth
<<required, ephemeral>>
Redis
<<S3, optional>>
Blob storage
External services
Section titled “External services”Supabase owns identity. Create a project and collect its connection string, project URL, and
a server-only secret key. Since 2025-10-01 Supabase signs tokens with asymmetric keys verified
via JWKS, so on a new project you leave the legacy JWT secret blank; the backend detects the
algorithm per token. The exact env values and the pooler-vs-direct-host detail are in
docs/DEPLOY.md.
S3-compatible blob storage. Create a bucket and an API token. The bucket must already exist; the service only presigns URLs, it never creates buckets.
Configure
Section titled “Configure”Copy .env.example to .env and fill it in. The main groups:
# Database (Supabase connection string, asyncpg driver)DATABASE_URL=postgresql+asyncpg://...
# RedisREDIS_URL=redis://redis:6379/0
# Supabase Auth (verify only)SUPABASE_URL=https://your-project-ref.supabase.coSUPABASE_JWT_SECRET= # blank on projects from 2025-10-01 onwardSUPABASE_SERVICE_ROLE_KEY= # server-only secret key; never ship to clients
# Blob storage (S3-compatible)S3_ENDPOINT_URL=https://...S3_BUCKET=clipboard-blobsAWS_ACCESS_KEY_ID=...AWS_SECRET_ACCESS_KEY=...AWS_REGION=auto
# AppAPP_CORS_ORIGINS=tauri://localhost,http://localhost:1420DEFAULT_BLOB_QUOTA_BYTES=52428800 # 50 MB per userADMIN_API_KEY= # blank disables the admin endpointsAPP_CORS_ORIGINS must include the app’s origin, which is tauri://localhost by default. Leave
ADMIN_API_KEY blank unless you need the admin endpoints; blank makes them return 503.
The repo ships a Dockerfile, a dev docker-compose.yml (with local Postgres, Redis, and
MinIO), and a docker-compose.prod.yml. For a local stack:
docker-compose upThen apply migrations:
uv run alembic upgrade headCheck that it is running. A healthy server answers with status 200:
curl http://localhost:8000/internal/healthzMigrations are never applied automatically by the deploy: a merged migration has not reached the database until someone runs it. Apply them deliberately after deploying new code.
Code
<<with a migration>>
Merge to main
<<needs new schema>>
New code live
Database
<<not applied>>
Migration file
<<routes fail>>
Old schema
<<routes work>>
Current schema
TLS and the desktop app
Section titled “TLS and the desktop app”Put a reverse proxy in front of the service for TLS (the sample setup uses Caddy for automatic certificates). Then point the app at your API, as below.
Point the app at your server
Section titled “Point the app at your server”The app needs three values, and they are compiled in, so users never type them:
| Value | Where to find it |
|---|---|
| Server URL | Your deployed API, for example https://sync.example.com |
| Supabase project URL | Supabase, Settings, API Keys, Project URL |
| Supabase publishable key | Supabase, Settings, API Keys. It starts with sb_publishable_; older projects call it the anon key |
Never use the secret key (sb_secret_). It gives full access to your project, and anything compiled into the app can be read by anyone who has a copy.
- Set the three constants at the top of
src-tauri/src/sync/config.rsin the app:These values are safe to commit. If they are empty, the Account & Sync screen says the build has no sync endpoints.const DEFAULT_SERVER_URL: &str = "https://sync.example.com";const DEFAULT_SUPABASE_URL: &str = "https://your-project-ref.supabase.co";const DEFAULT_SUPABASE_ANON_KEY: &str = "sb_publishable_..."; - Build or run the app. Sign up on the Account & Sync screen; your account and this device should appear.
To point an installed copy at a different server without rebuilding, set sync_server_url,
supabase_url and supabase_anon_key in settings.json in the app’s data folder. There is no
screen for this on purpose.
Set up Google sign-in
Section titled “Set up Google sign-in”Google sign-in needs all three of these, or it fails after the consent screen with no useful error:
- In Supabase, under Authentication, Providers, enable Google with the client ID and secret from Google Cloud Console.
- In Google Cloud Console, add
https://<project-ref>.supabase.co/auth/v1/callbackto the OAuth client’s authorized redirect URIs. - In Supabase, under Authentication, URL Configuration, add all three of
http://127.0.0.1:53170,http://127.0.0.1:53171andhttp://127.0.0.1:53172to the redirect URLs.
<<opens your browser>>
Desktop app
<<consent screen>>
<</auth/v1/callback>>
Supabase
<<signed in>>
Desktop app
<<first free port>>
127.0.0.1
Step 3 is the easy one to miss. The app receives the sign-in on the first free port of those
three on the user’s computer. If they are not allowed, Google’s consent succeeds but Supabase
will not send the user back, and the app waits until it times out after 5 minutes. The ports are
fixed in src-tauri/src/sync/oauth.rs.
Deeper operations
Section titled “Deeper operations”Migrations in detail, the build-on-box deploy pipeline, rollback, log retention, metrics, and
troubleshooting are all in the backend
docs/DEPLOY.md.
That runbook documents one concrete setup; adapt the host-specific parts to wherever you run it.