A relay is optional. Two organizers can sync device-to-device with no server
at all (habitable sync … --dir <shared folder>). Run a relay only when peers
can't reach each other directly — for example, two phones on different networks.
The relay can read nothing. Every message is sealed to a peer's key before it
arrives; the relay stores and forwards opaque ciphertext per room and keeps only
aggregate passthrough counts. It still sees connection metadata (which rooms are
busy, when, and message sizes) — see threat-model.md. The
image is dependency-free (standard library only), runs as a non-root user, and is
read-only with all capabilities dropped.
From the repository root:
$ docker compose -f relay/docker-compose.yml up -d --build
$ curl -s http://localhost:8787/healthz
{"status": "ok", "rooms": 0, "posted": 0, "fetched": 0, "bytes_relayed": 0}Or without compose:
$ docker build -f relay/Dockerfile -t habitable-relay .
$ docker run -d --name habitable-relay -p 8787:8787 \
--read-only --cap-drop ALL --security-opt no-new-privileges:true \
habitable-relayWithout Docker, the relay is one command (no install needed beyond the source):
$ HABITABLE_RELAY_HOST=0.0.0.0 uv run habitable relay --host 0.0.0.0 --port 8787Each organizer runs habitable id, shares their public-id, verifies the
fingerprint out of band, then syncs through a shared room id:
$ uv run habitable sync --vault ./case-4B --peer <their-public-id> \
--channel <shared-room-id> --relay https://relay.example.org- TLS: terminate HTTPS at a reverse proxy (Caddy/nginx/Traefik) in front of
:8787. Peers should use anhttps://relay URL; the client refuses non-HTTP(S) URLs but does not add TLS itself. - Health:
GET /healthzreturns200with passthrough metrics; the container ships aHEALTHCHECKthat polls it. - Storage: messages are held in memory (capped per room) and forwarded; the relay is not a backup. Restarting it drops undelivered messages — peers simply re-sync (sync is idempotent).
- Logs: the relay does not log request lines or contents, only aggregate
counts via
/healthz. Keep it that way; a no-log relay is part of the privacy posture. - Scaling: it scales to zero between sessions; a union can run one small instance, or none at all.
The relay is not a place your case lives. There is no account, no plaintext, and no authority over a union's records on it — forking or self-hosting changes nothing about who can read the data: still only the keyholders.