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

  1. 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-js directly.
  2. The Supabase CLI locally, to push the schema migrations to your Supabase project. Install: supabase.com/docs/guides/local-development/cli
  3. 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.
  4. Docker, or anywhere that can run a Node 22 container (Fly.io, Railway, ECS, Cloud Run, plain VPS).
  5. 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 keySUPABASE_ANON_KEY
  • service_role secret keySUPABASE_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 push
bash

⚠️ 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:latest
bash

Health 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',
})
ts

Environment variables

VariableRequiredDescription
SUPABASE_URLYesYour Supabase project URL (https://<ref>.supabase.co)
SUPABASE_SERVICE_ROLE_KEYYesService role key — used by the logger to write to requests past RLS
SUPABASE_ANON_KEYYesAnon key — used for RLS-protected reads from dashboard queries
ENCRYPTION_KEYYes32-byte base64 key for AES-256-GCM provider-key encryption at rest
WEB_URLYes (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_KEYNoResend (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_FROMNoSender 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.
PORTNoHTTP 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 push
bash

We 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 dev with NEXT_PUBLIC_API_URL pointed 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-js directly. 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-migrate tool is a post-launch priority.
  • No spanlens-web Docker 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.