Public WebRTC signaling relay + guest entry point for Familiar listening sessions.
A Familiar host (running on their NAS, laptop, etc.) opens a WebSocket to this service to create a session. The host shares a code with friends. Friends visit https://<this-service>/listen/<code> and join. The service relays SDP/ICE between peers; audio flows peer-to-peer over WebRTC and never touches this server.
Familiar's backend usually isn't publicly reachable (typical setup: a personal NAS behind Tailscale). That's fine for the host, but their friends aren't on Tailscale. This service decouples the signaling rendezvous from the host's private backend so anyone with a link can join.
- FastAPI + Uvicorn (Python 3.11+) — WebSocket signaling, ~400 LOC
- React + Vite + Tailwind (TypeScript) — standalone guest SPA, ~78 KB gzip
- Single Fly.io VM, 512 MB RAM. No database. Sessions in-memory; cap of 50 concurrent. State is lost on restart.
| Path | Type | Notes |
|---|---|---|
/ |
HTML | Splash + "enter a code" form |
/listen/:code |
HTML | Guest SPA route |
/api/v1/sessions/ws |
WebSocket | Signaling (create / join / playback / chat / kick / WebRTC SDP+ICE) |
/api/v1/sessions/by-code/{code} |
GET JSON | Public lookup. Returns name, participant count, has_password. Excludes participants and TURN credentials. |
/health |
GET JSON | Liveness |
See app/routes.py for the full WebSocket protocol.
Anyone can create or join_guest — the session code is the only credential. Hosts can optionally set a password (bcrypt-hashed). Hosts can kick participants. There is no account system.
# backend
uv sync
uv run uvicorn app.main:app --reload --port 8000
# guest SPA (separate terminal)
cd guest
pnpm install
pnpm dev # http://localhost:5173, proxies /api → :8000For a single-process dev experience, build the guest SPA once and let FastAPI serve it:
cd guest && pnpm install && pnpm build && cd ..
uv run uvicorn app.main:app --reload --port 8000
# open http://localhost:8000uv run pytest -vWithout TURN, ~30% of guests behind symmetric NAT (corporate, mobile carriers) won't connect. Two ways to wire it up — pick one (or both, they're additive):
Cloudflare Realtime TURN (recommended; free tier covers 1 TB/month). Create a TURN key in the Cloudflare dashboard, then:
fly secrets set CLOUDFLARE_TURN_TOKEN_ID=... \
CLOUDFLARE_TURN_API_TOKEN=... \
-a familiar-sessionsThe relay mints short-lived ICE credentials via Cloudflare's REST API and caches them per-process for ~21.6 h before refresh. Failed refreshes fall back to the last good cache so a Cloudflare outage doesn't take the relay down.
Self-hosted coturn / static TURN — for setups where you'd rather run your own:
fly secrets set TURN_SERVER_URL=turn:turn.example.com:3478 \
TURN_SERVER_USERNAME=... \
TURN_SERVER_CREDENTIAL=... \
-a familiar-sessionsfly launch --no-deploy # first time only
fly deployThe Familiar frontend's useListeningSession hook needs to point at this service:
VITE_SESSIONS_RELAY_URL=https://familiar-sessions.fly.dev pnpm -C packages/web buildHosts continue to load Familiar from their own backend; only the signaling WebSocket and the guest-facing URL come from here.