Self-hosting
Run the Spanlens proxy + API on your own infra. Keeps all request bodies, traces, and encrypted provider keys inside your network. The hosted dashboard at spanlens.io can then read from your self-hosted backend.
⚠️ Early access
The proxy server image is public and boots end-to-end (verified 2026-04-22). Rough edges: Supabase is required (plain Postgres isn't supported yet), migrations aren't bundled in the image, and a separate dashboard image isn't published yet. Walk through the steps below; if you hit friction, file a GitHub issue and we'll smooth it.
Who should self-host
- Compliance requirements (SOC 2, HIPAA, data residency) forbid sending LLM bodies through a third-party SaaS
- You already run Supabase in-house
- You expect traffic volumes where per-request pricing on the hosted plan exceeds the cost of running your own infra
What you need
- A Supabase project. The free tier on supabase.com is enough to start; power users can also self-host the full Supabase Docker stack on their own Postgres. Plain Postgres is not supported — the server uses
@supabase/supabase-jsdirectly. - The Supabase CLI locally, to push the schema migrations to your Supabase project. Install: supabase.com/docs/guides/local-development/cli
- A 32-byte encryption key. Used for AES-256-GCM encryption of provider keys at rest. Generate with
openssl rand -base64 32. Back this up. Losing it makes every stored provider key unrecoverable. - Docker, or anywhere that can run a Node 22 container (Fly.io, Railway, ECS, Cloud Run, plain VPS).
- A reverse proxy with HTTPS in front (Caddy, nginx, Cloudflare Tunnel). The container speaks HTTP on port 3001.
Walkthrough
1. Create a Supabase project
Sign in at supabase.com, create a project, wait for it to provision (~1 minute).
From Project Settings → API, copy:
- Project URL → will be your
SUPABASE_URL - anon public key →
SUPABASE_ANON_KEY - service_role secret key →
SUPABASE_SERVICE_ROLE_KEY(keep this server-side only)
2. Apply the schema migrations
Clone the repo to get the migration SQL files, then link your Supabase project and push:
git clone https://github.com/sunes26/Spanlens.git
cd Spanlens
# One-time: log in and link to your Supabase project
supabase login
supabase link --project-ref <your-ref> # "ref" is the <ref>.supabase.co subdomain
# Apply all migrations
supabase db pushbash⚠️ Known gap: migrations aren't bundled in the Docker image yet, so this manual step is required. Roadmap: ship a separate spanlens-migrate image that applies them for you.
3. Run the server
docker run -d --name spanlens \
-p 3001:3001 \
-e SUPABASE_URL="https://<your-ref>.supabase.co" \
-e SUPABASE_ANON_KEY="eyJhbGciOi..." \
-e SUPABASE_SERVICE_ROLE_KEY="eyJhbGciOi..." \
-e ENCRYPTION_KEY="$(openssl rand -base64 32)" \
ghcr.io/sunes26/spanlens-server:latestbashHealth check: curl http://localhost:3001/health should return {"status":"ok"}.
Verified 2026-04-22: the image pulls without auth and boots against fake env vars past the DB init check. If you prefer building from source, docker build -f apps/server/Dockerfile -t spanlens-server . from the repo root works too.
4. Point your application at the self-hosted proxy
Any SDK or direct proxy pattern works — replace the default base URL with your self-hosted domain:
import { createOpenAI } from '@spanlens/sdk/openai'
const openai = createOpenAI({
baseURL: 'https://spanlens.yourcompany.com/proxy/openai/v1',
})tsEnvironment variables
| Variable | Required | Description |
|---|---|---|
SUPABASE_URL | Yes | Your Supabase project URL (https://<ref>.supabase.co) |
SUPABASE_SERVICE_ROLE_KEY | Yes | Service role key — used by the logger to write to requests past RLS |
SUPABASE_ANON_KEY | Yes | Anon key — used for RLS-protected reads from dashboard queries |
ENCRYPTION_KEY | Yes | 32-byte base64 key for AES-256-GCM provider-key encryption at rest |
WEB_URL | Yes (multi-user) | Base URL of your dashboard (e.g. https://spanlens.example.com). Used to build the accept link in invitation emails. Falls back tohttp://localhost:3000 if unset — fine for local dev, broken in production. |
RESEND_API_KEY | No | Resend (resend.com) API token for outbound email (currently invitations). When unset, emails are skipped silently and the invite endpoint returns the accept link as devAcceptUrlin its JSON response so an admin can hand-deliver it. |
RESEND_FROM | No | Sender header for outbound email. DefaultSpanlens <notifications@spanlens.io>. Override with a verified sender on your own domain (e.g.Spanlens <notifications@mail.example.com>) — unverified senders land in spam folders. |
PORT | No | HTTP port (default 3001) |
Upgrading
docker pull ghcr.io/sunes26/spanlens-server:latest
docker restart spanlens
# If new migrations shipped, re-pull the repo and push:
cd Spanlens && git pull && supabase db pushbashWe ship semver tags (ghcr.io/sunes26/spanlens-server:0.3.0). Pin a tag in production and upgrade deliberately.
Dashboard access
Two options today:
- Use the hosted dashboard at spanlens.io pointed at your self-hosted backend. Log in, then override the API base URL in your browser via /settings.
- Run the web app locally yourself — clone the repo and
pnpm --filter web devwithNEXT_PUBLIC_API_URLpointed at your backend.
⚠️ Known gap: a separate ghcr.io/sunes26/spanlens-web image is planned but not yet published. Earlier versions of these docs claimed it existed — that was aspirational, not reality.
Backups
Everything persists in Postgres. Standard pg_dump against your Supabase DB covers you. The critical thing to back up outside the DB is ENCRYPTION_KEY — without it, encrypted provider keys are unrecoverable. Store it in your secret manager (AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault) with a rotation schedule you can follow.
Known gaps & roadmap
Honest current state of self-host:
- Plain Postgres isn't supported. The server imports
@supabase/supabase-jsdirectly. Moving to a thin Postgres abstraction is on the roadmap but not a launch blocker. - Migrations ship separately (via Supabase CLI + repo clone). A bundled
spanlens-migratetool is a post-launch priority. - No
spanlens-webDocker image yet. Use the hosted dashboard or run from source. - Operational tooling is minimal. No built-in monitoring, no migration rollback tool, no backup cron. DIY for now.
Found a problem? Open an issue on GitHub.