wss://backroom.jck.xyz/ws
A dumb pipe,
on purpose.
Backroom Relay is a WebSocket rendezvous point for devices that encrypt end to end. Two peers behind NAT dial out, meet in a room named by a secret only they hold, and every frame is forwarded byte for byte. The relay holds no keys, reads nothing, and stores nothing.
Two devices, neither reachable
The relay was built for Backroom, a macOS agent app with an iOS companion. The phone needs to reach the Mac from anywhere, and the Mac sits behind NAT where nothing can dial it. So both ends connect out to this relay, present the same room id, and the relay pipes frames between them.
Nothing about it is Backroom-specific. Any two endpoints that already share a secret can derive a room id from it, seal their traffic end to end, and use the relay as a meeting point. It is a generic pipe, and it is documented as one.
Confidentiality is not the relay's job
The relay is structurally unable to read traffic, not merely promising not to. There is no key material anywhere in the codebase, and no code path that parses, transforms, or logs a frame's payload. The access model has two parts, both deliberate:
- End-to-end encryption on the endpoints provides confidentiality and integrity. Every byte crossing the relay is ciphertext sealed under a key derived from an offline pairing.
- The room id is an unguessable capability. It is a high-entropy secret the two devices already share. The relay admits at most two connections per room, so even a party who somehow learns a room id cannot join an established pair.
There is no relay-side auth secret, and that is a decision, not an omission: a shared relay secret would have to be distributed to every client, which is strictly worse than a per-pair capability. The full security model spells out the threat model and what each class of attacker actually gets.
Stated plainly, because "end to end" is often read as "invisible"
The relay can see
- The room id and therefore that a pair is talking. Logged only as a six-character prefix, never in full.
- Frame sizes and timing the shape of the traffic, not its content.
- The source IP of both ends used for per-IP rate limiting and nothing else.
The relay can never see
- Keys no key material exists anywhere in the relay codebase.
- Plaintext method, path, body: all sealed before they reach the wire.
- History nothing is persisted. State is an in-memory map that dies with the process.
No amount of relay compromise turns into commanding the Mac: a forged frame does not decrypt, and is dropped before anything acts on it.
Built to refuse features
No database. No persistence. No accounts. No auth server. No message history. No rooms-list endpoint. No admin API. State is an in-memory map of room id to its at-most-two peers, and nothing survives a restart. That is correct behaviour: a dropped connection just reconnects.
Every one of these omissions is load-bearing for the dumb-pipe security model. A relay that could list rooms or replay history would be a relay worth compromising.
The entire HTTP surface
| Endpoint | Purpose |
|---|---|
GET /ws?room=<id> |
The one WebSocket endpoint. Binary frames are forwarded verbatim to the other peer in the room; text frames from the relay signal presence. |
GET /healthz |
Liveness probe. Returns 200 ok plus an X-Relay-Rev header naming the deployed commit. Reveals nothing about rooms or traffic. |
That is all of it. There is deliberately no endpoint that enumerates rooms or peers. The client contract covers every response, close code, and control frame a client can encounter.
Documentation
How it works
Rooms of two, verbatim forwarding, presence signals, and the liveness machinery that keeps a tunnel alive through Cloudflare and cellular NAT.
D-02Security model
The dumb-pipe principle, the capability room id, the threat model, and exactly what each class of attacker can and cannot do.
D-03Protocol
The generic client contract, and the sealed tunnel protocol Backroom's two apps speak through the relay: key derivation, frame format, replay bounds.
D-04Operations
Every configuration variable, the test-gated deploy pipeline with automatic rollback, and the nginx details WebSockets always need.