ADR-PROD-004 - WebRTC stack for guests: LiveKit
How to read this page
A Plain and a Clear version of this page have not been written yet. What follows is the document itself.
Precise — the source document
This is the document. Rendered from the repository at the commit above, with nothing rewritten for the web. A gate re-renders it on every deploy and fails the build if a single byte differs.
- Status: Proposed
- Date: 2026-06-21
- Deciders: UNI Production architecture
- Master contract:
docs/UNI_PRODUCTION_PLATFORM.md(decision 2; unituni-bcast-livekit)
Context
Remote guests must join a broadcast by opening a simple UNI.OS-hosted page, connecting cam+mic, and
authenticating; the host admits them from a green room (off-air, host-visible) to on-air, where a
stage page lays out talking-head (one guest) or panel (N guests). OBS must remain the single mixer
(ADR-PROD-001), so the guest layer must produce something OBS can capture as a browser-source rather than
becoming a second mixer. The platform is free/open and self-hosted on UNI.OS as a quadlet, behind the
appliance's loopback/WireGuard posture.
Decision
Use LiveKit (livekit/livekit-server) as the WebRTC SFU for guests (uni-bcast-livekit: ws/http
:7880, rtc-tcp :7881, rtc-udp 50000-50200). Apache-2.0, self-hostable in one quadlet. Its room
model maps directly to green-room -> on-air: a green-room room and an on-air room. The host admits a guest
green-room -> on-air via the production MCP admit_guest verb (human-gated, outward-facing -
ADR-PROD-010). The production/guest/ join app uses the LiveKit JS SDK (cam/mic check in the green room);
the stage page (:8099/overlays/stage.html) subscribes to the on-air room and lays out
talking-head/panel in 2D/CSS (ADR-PROD-005); OBS captures the stage page, so OBS stays the only
mixer. Simulcast and server-side admin (admit/remove) come from LiveKit.
Alternatives considered
- mediasoup. Rejected: it is a low-level SFU library - you build all signaling, room lifecycle, and layout yourself. More control, much more bespoke code to write, test, and secure; the green-room -> on-air room mapping would be hand-rolled.
- Janus. Rejected: capable and battle-tested but older ergonomics, plugin-centric config, and a less direct room model for the green-room -> on-air flow; the JS SDK story is weaker than LiveKit's.
- Jitsi (already on the appliance). Rejected as the guest SFU: Jitsi is part of the protected business stack (read-only, never a mutation target) - reusing it would couple the broadcast to the business stack and violate the charter boundary. The broadcast node runs its own LiveKit instead.
- LiveKit Egress as the mixer. Out of scope here (rejected in ADR-PROD-001 as the mixer); LiveKit is used for guest transport only, and OBS composites the resulting stage page.
Consequences
- One quadlet, a direct green-room -> on-air room model, simulcast, a mature JS SDK, and server-side
admit/remove - the guest flow is straightforward to drive from the MCP. Honest tradeoff: WebRTC needs a
UDP range (
50000-50200) reachable by guests, which is firewall/NAT surface to manage on the broadcast node; behavior is pending until a guest join is observed end-to-end (P3 exit check). - OBS stays the single mixer because the guest layout is just a captured 2D/CSS page - no second compositor.
- The Jitsi business-stack boundary is preserved: the broadcast runs its own LiveKit; Jitsi is not touched.
- Evidence class: LiveKit capabilities are vendor-documented (design reference); "a guest joins green room and is admitted to a panel" is pending until the P3 captured run.
Links
- Master:
docs/UNI_PRODUCTION_PLATFORM.md - Apps:
production/guest/(green-room join),production/overlays/stage.html(on-air layout) - Related: ADR-PROD-001 (OBS captures stage page), ADR-PROD-005 (2D/CSS layout), ADR-PROD-010
(
admit_guesthuman-gated) - Quadlet:
production/containers/systemd/uni-bcast-livekit.container
Status (honest)
This ADR is a design, status pending; nothing is deployed or claimed to run. No banned-unqualified
word is used as a claim. LiveKit capability statements are design references; the end-to-end guest flow is
pending until a P3 captured run. The business stack (solutionwright-*, odoo, jitsi, cloudflared,
portainer) is never a mutation target - the broadcast runs its own LiveKit and does not reuse Jitsi; the
producer agent cannot self-approve, and admit_guest is always human-gated.
sha256 b2f483452b489067 — of the original file, so what was ingested stays checkable.