server · Go · self-hosted
Relay does one thing: it holds the desktop's outbound connection, matches it to the client's, and moves bytes between them. Pair a browser and those bytes are ciphertext it has no key for; serve the OpenAI-compatible endpoint and it reads them to speak HTTP. Either way, it is one Go binary you can run yourself.
# one container, both pathsdocker run -d -p 8080:8080 \ -e RELAY_ALLOWED_ORIGINS=https://your.app \ ghcr.io/41tunnels/relay:latest# then, in Amallo → Settings → Relay:# wss://relay.example.comDesktop dials in and waits; a client arrives with a matching pair ID; Relay joins the two sockets and gets out of the way.
Pair a browser and the session key never touches Relay: it moves AES-256-GCM frames and knows nothing about their contents.
No prompts, no completions, no transcripts. Connection metadata only, and only while the connection is open.
The pairing code is the credential, and it lasts until you replace it. Regenerate it in Amallo and every client holding the old one is locked out. Handshakes get five seconds to complete, and new connections are rate-limited per IP.
Serving plain OpenAI HTTP means Relay can read that traffic: a third-party client has no shared secret to encrypt with. It is on by default, because it is what most people install Amallo for, so it is named as a tradeoff rather than buried. Turn it off with RELAY_HTTP_ENABLED, or host the box yourself.
The wire format is written down in spec/PROTOCOL.md and both clients implement it independently against shared test vectors. Where code and spec disagree, the spec wins.
Every release publishes a multi-arch image to the GitHub Container Registry, tagged with the version and :latest, for linux/amd64 and linux/arm64.
ghcr.io/41tunnels/relay:latest
One Go module, no CGO, no external dependencies at runtime. Defaults are usable out of the box; override with RELAY_* environment variables for anything internet-facing.
go run ./cmd/relay
Relay expects a reverse proxy that terminates TLS and passes WebSocket upgrades through untouched. The repo ships a Caddyfile.example that also notes the two things nginx and Traefik need to get right.
GET /stats is a deliberately aggregate-only status page: pairs, agents, clients, uptime, and nothing else. Ours is public →
Public listener.
WebSocket origin allowlist. Open by default: the pairing secret is what gates a connection, not the browser origin.
Serve the OpenAI-compatible HTTP endpoint. The one setting that changes what Relay can see.
Capacity cap on new pair creation.
Prometheus listener, bind to loopback only.
Trust X-Forwarded-For from the reverse proxy in front.
Config is validated at startup; a malformed value fails the boot rather than silently falling back. Full list →
source, deploy notes and flags on GitHub