backroom / relay / docs / operations
Operations
One static Go binary, configured entirely by environment variables, deployed by git push behind a test gate with automatic rollback. This page is generated from the same repository as the code and is checked against it by the test suite.
Configuration
All configuration is environment variables, twelve-factor style, because the relay runs as a managed daemon. There are no config files. Every knob, with its default:
| Variable | Default | Meaning |
|---|---|---|
RELAY_LISTEN_ADDR | 127.0.0.1:8787 | Bind address. In production this is localhost and nginx fronts it. |
RELAY_MAX_MESSAGE_BYTES | 1048576 | Max size of a single frame (1 MiB). A public endpoint without this is a trivial memory-exhaustion target. |
RELAY_MAX_ROOMS | 4096 | Global cap on concurrent rooms, so a flood of distinct room ids cannot grow the registry without bound. |
RELAY_SEND_BUFFER | 32 | Frames that may queue for a peer before the pair is torn down for backpressure. Small on purpose: the relay is a pipe, not a mailbox. |
RELAY_CONN_RATE_PER_IP | 1 | Sustained new-connection rate per IP, per second. |
RELAY_CONN_BURST | 20 | Connection burst allowance per IP. |
RELAY_PING_INTERVAL | 30s | How often an idle peer is pinged. Proves the peer is alive. |
RELAY_KEEPALIVE_INTERVAL | 45s | How often a peer is sent a keepalive data frame. Keeps the network path open, which pings do not: Cloudflare and cellular NAT ignore control frames and drop an idle tunnel at about 100 seconds. 0 disables it. |
RELAY_WRITE_TIMEOUT | 10s | Per-frame write deadline. A peer slower than this is treated as dead. |
RELAY_PING_TIMEOUT | 10s | Deadline for a ping round trip. |
RELAY_ROOM_ID_MIN_LEN | 16 | Minimum room id length: the entropy floor. |
RELAY_ROOM_ID_MAX_LEN | 256 | Maximum room id length. |
RELAY_TRUST_PROXY_HEADER | (empty) | If set (for example X-Real-IP), the header a trusted proxy uses for the real client IP. Required for meaningful per-IP limits behind nginx. Leave empty on a raw public bind, or clients could spoof it. |
Running locally
go build -o backroom-relay .
RELAY_LISTEN_ADDR=127.0.0.1:8787 ./backroom-relay
For local testing over wss:// there is a dev-only flag that
mints a throwaway self-signed certificate in memory, never written to disk
and never used in production:
./backroom-relay --tls
The tests start the real server and exchange bytes over real WebSocket connections, rather than asserting against mocks:
go test -race ./...
The deploy pipeline
Deploying is git push. The server builds from its own
checkout; no binary is committed or uploaded by hand, and the deploy logic
lives in the repository where it is reviewable, not in a web text box.
git push origin main
│
▼ Forge pulls, runs deploy/forge-deploy.sh
go test ./... red tests abort here, live binary untouched
│
▼
go build → backroom-relay.new → atomic rename over backroom-relay
│
▼
restart daemon → poll /healthz → compare X-Relay-Rev to the pushed commit
│ │
▼ healthy ▼ not healthy
drop .prev, done restore .prev, restart, fail loudly
X-Relay-Rev on /healthz names the commit the
running binary was built from, so "did my deploy actually take?" is one
request:
curl -sI https://backroom.jck.xyz/healthz | grep -i x-relay-rev
The test suite includes a documentation check: every
RELAY_* variable read by the code must appear on this page
and in the README, or the tests fail. Since deploys run the tests before
building, a configuration change cannot reach production with stale
documentation.
The nginx essentials
The binary speaks plain WS on localhost; nginx terminates TLS at the edge. Three things bite everyone who fronts a WebSocket with nginx, and all three are handled in the repository's config:
-
The upgrade handshake. Without
proxy_http_version 1.1and theUpgrade/Connectionheaders, nginx buffers the connection and the101never happens. -
Idle timeouts. WebSocket connections can sit idle for
minutes between frames. Without long
proxy_read_timeoutandproxy_send_timeoutvalues, nginx reaps an idle-but-alive tunnel after its default 60 seconds. -
Real client IPs. Behind a CDN every connection arrives
from an edge address, which would bucket the whole world into a handful
of IPs for rate limiting.
real_ip_headerrestores the true client address, trusted only from the published edge ranges, which is what makes it unspoofable: a client connecting straight to the origin cannot forge it. nginx then overwrites (never appends)X-Real-IPbefore proxying, and the relay reads that header only whenRELAY_TRUST_PROXY_HEADERopts in.
Self-hosting checklist
The relay is a generic pipe, so running your own is deliberately boring:
- Build the static binary:
CGO_ENABLED=0 go build -ldflags="-s -w" -o backroom-relay . - Run it as a daemon bound to localhost, environment variables as above. A systemd unit ships in
deploy/. - Front it with nginx terminating TLS, with the WebSocket upgrade headers, the long timeouts, and (if behind a CDN) the real-IP block.
- Set
RELAY_TRUST_PROXY_HEADER=X-Real-IPso per-IP limiting sees real clients. - Point an uptime check at
/healthz.
There is nothing else: no database to provision, no secrets to distribute, no state to back up. Restarting the relay drops connections and the endpoints reconnect.