The relay is the only piece of telecode that could run on someone else's infrastructure — and it only ever sees ciphertext + routing metadata (see threat-model.md). Running your own removes even the metadata exposure to a third party. The bundle brings up the relay and its Postgres with one command.
- Docker + Docker Compose.
cp infra/.env.example infra/.envFill in infra/.env. The required secrets (generate each with openssl rand -base64 32):
| Variable | Purpose |
|---|---|
POSTGRES_PASSWORD |
password for the bundled Postgres |
CHANNEL_TOKEN_SECRET |
signs short-lived browser channel tokens |
RELAY_SERVICE_SECRET |
shared secret the web tier presents on server-to-server calls |
Optional features turn on only when their secret is present (the relay logs which are off at startup):
| Variable | Enables |
|---|---|
TOKEN_ENCRYPTION_KEY (base64 32-byte) |
encrypted GitHub-token storage + repo listing in the launch picker |
VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY |
web push notifications (npx web-push generate-vapid-keys) |
infra/.env is git-ignored — never commit it.
docker compose -f infra/docker-compose.yml upThis starts Postgres, Redis, applies migrations once (a one-shot migrate step), then starts the relay on
http://localhost:8080 (change the host port with RELAY_PORT). Verify it:
curl -fsS http://localhost:8080/healthz # → {"status":"ok"}The relay is the only publicly reachable piece, so it ships safe to leave running:
- Rate limiting is on by default — every HTTP request is limited per client IP (default 300 / minute; the pairing endpoints are tighter). The bundle wires Redis automatically so the budget is shared across relay instances; a single relay also works without Redis (in-memory fallback).
- Set
TRUST_PROXY=truebehind a reverse proxy (the bundle defaults to this). It makes the per-IP limit key on the real client fromX-Forwarded-For. If you set it false while behind a proxy, every client shares one budget; if true with no proxy, a client could spoof its IP — match it to your topology. - Set
RATELIMIT_ALLOWLISTto your web tier's egress IP(s). The web tier calls the relay on behalf of every user, so its traffic aggregates under one IP — without exempting it the limiter could throttle your whole user base. Tune the budget withRATELIMIT_MAX/RATELIMIT_WINDOW, or disable entirely (not recommended) withRATELIMIT_DISABLED=true. - More abuse limits are on by default: a request body cap (
BODY_LIMIT, default 64 KB — the relay's bodies are tiny), a per-IP concurrent WebSocket cap (MAX_WS_CONNECTIONS_PER_IP, default 32), and a brute-force lockout on device pairing approval. These need no configuration for a typical deployment.
- Daemon (the laptop running agents): point it at your relay with the
--relay-urlflag, e.g.npx @telecode/cli --relay-url wss://relay.example.com/ws(or setTELECODE_RELAY_URL; the flag wins). The URL must bews://orwss://. On first run it prints a pairing code; enter it in the web app to bind the device. - Web app: set
PUBLIC_TELECODE_RELAY_URLto the same relay, and — if you enabled push —PUBLIC_VAPID_KEYto yourVAPID_PUBLIC_KEY. GitHub OAuth (sign-in + the repo token) is configured in the web tier; the relay only stores the resulting token, encrypted, whenTOKEN_ENCRYPTION_KEYis set.
The web UI is deployed separately from this bundle (it is a SvelteKit app, not part of the relay image).
For local use, run it with pnpm --filter web dev pointed at your relay.
- TLS: in production, terminate TLS in front of the relay (a reverse proxy) and use
wss:///https://URLs — channel and device tokens are bearer credentials. - Upgrades:
docker compose -f infra/docker-compose.yml up --buildrebuilds the relay image and re-runs the one-shot migration step before the new relay starts. Data persists in thetelecode-pgdatavolume. - The relay never executes agents and never holds private keys — it is a stateless multiplexer plus the device/session registry. Agent work stays on your machine.