Deploy the relay
One Docker container, plus DNS and TLS. Stateless, JWT-validated, restartable any time.
This page is the single-container deployment quickstart. For the broader story of what self-hosting the relay gets you, see Self-host overview.
What the relay actually is
viewport/services/relay/, node 22 multi-stage Dockerfile.
Properties:
- Operationally stateless. Per-process state (in-memory connection registry, IP rate-limit counters, recent log buffer, metrics counters) is lost on restart. No payloads persisted.
- WebSocket upgrade only at
GET /ws. Validated with a Zod schema for query params (role,workspaceId,runtimeTargetId?). - Frames are E2EE. Validated by
relay-frame-validation.ts; the relay can identify envelopes (workspace, runtime target) but cannot decrypt. - Auth is JWT-based. The relay calls the platform's
/api/runtime/internal/relay/validateto admit each upgrade. - Cross-relay backplane for multi-instance deploys:
RELAY_BACKPLANE_MODE=server|redis.
Minimum viable deployment
docker run -d \
--name viewport-relay \
--restart unless-stopped \
-p 7781:7781 \
-e RELAY_LISTEN_PORT=7781 \
-e RELAY_INTERNAL_KEY="<shared secret with platform>" \
-e RELAY_VALIDATE_URL="https://api.getviewport.com/api/runtime/internal/relay/validate" \
-e RELAY_JWKS_URL="https://api.getviewport.com/api/.well-known/jwks.json" \
ghcr.io/viewportai/relay:latestPoint your DNS / load balancer at the container and terminate TLS in front (the relay itself can run plain WS behind your TLS terminator).
Configuration
The full env-var set is documented in services/relay/README.md in the OSS repo. The most important ones:
| Env | Purpose |
|---|---|
RELAY_LISTEN_PORT | Port to bind |
RELAY_INTERNAL_KEY | Shared secret used to authenticate calls to the platform's relay-internal endpoints |
RELAY_VALIDATE_URL | Where to validate JWTs on each upgrade |
RELAY_JWKS_URL | Where to fetch signing keys |
RELAY_ADMIN_TOKEN | Bearer token to access /state, /logs, /metrics |
RELAY_ENABLE_ADMIN_HTTP | 1 to expose admin endpoints; default off |
RELAY_BACKPLANE_MODE | single | server | redis |
RELAY_BACKPLANE_URL | Backplane endpoint (when server or redis) |
RELAY_CLIENT_REDIRECT_ENABLED | 1 to redirect clients to the relay holding their daemon (multi-instance) |
Admin endpoints
When RELAY_ENABLE_ADMIN_HTTP=1:
| Method | Path | Auth | Returns |
|---|---|---|---|
| GET | /health | none | {ok, service, uptimeMs, tlsEnabled?, relayMode?, relayId?} |
| GET | /state | bearer | Snapshot of registered workspaces, daemon connection state, client counts |
| GET | /logs | bearer | Recent log buffer (?summary=1 for short form) |
| GET | /metrics | bearer | Prometheus exposition |
If admin is disabled the routes return 404, not 401. Informationally indistinguishable from a non-existent server.
Pointing the daemon at your relay
vpd remote login --relay-url wss://relay.your-co.com/wsOr per-workspace by overriding ~/.viewport/config.json daemon.relay.endpoint. See CLI · remote.
Multi-relay topologies
Two backplane modes for running more than one relay:
server: persistence-backed bus through the platform API.relay_bus_framesandworkspace_relay_presencestables on the platform side fan frames between relays. Use when relays are in different regions and Redis is too much to operate.redis: pub/sub bus on a shared Redis. Lower latency, requires Redis ops.
When the same workspace has a daemon on relay-A and a browser on relay-B, the backplane carries the frames. With RELAY_CLIENT_REDIRECT_ENABLED=1, the relay tells the browser to reconnect to the relay that holds its daemon. Keeps the hot path single-hop.
What's still hosted
These calls still go to api.getviewport.com even when running your own relay:
- All user-facing REST routes (
/api/workspaces,/api/plans,/api/inbox-items, etc.) - Daemon → platform sync (
/api/runtime/*) - Relay → platform JWT validation (
/api/runtime/internal/relay/validate) - Audit log writes
If you need any of those on-prem, the relay-self-host path is not enough. Get in touch.
Crypto for the security-curious
The wire envelope between daemon and relay can be either:
noise-ik: Noise IK patternnoise-ikpsk2: Noise IKpsk2 (selected when workspacepolicy_mode = 'crypto_pairing')
Conformance vectors: packages/daemon/docs/relay-noise-v3-conformance-vectors.json. Run npm run test:conformance to replay them against the daemon's compiled handshake code.
Related
- Quickstart for the pairing path the relay sits inside
- API for the auth model and JWT shapes
- Concepts: Context Vault for the bytes the relay can't read