Architecture العربية

ADR-0005 — Video Consultation Platform

Status: Proposed — conditional on OQ-08 and a legal ruling · Date: 2026-07-20 · Deciders: Amir Haroun (Tech Lead) Related: SEAM-02, QAS-AVL-01, QAS-SCL-02, QAS-MOD-02, R-10, ADR-0001


Context#

SRC-MOM FR-1 / REQ-03 requires in-app video consultations, "stable start to finish". The MoM (§11) proposed self-hosted Jitsi on open-source cost grounds. CON-13 requires KSA residency; scale target is hundreds of concurrent sessions within 12–18 months, from near zero at launch.

Research overturned three widely-held assumptions, including two the architect held going in. They are recorded prominently because acting on the original assumptions would have produced a materially worse system.


⚠ Three corrected assumptions#

Correction 1 — LiveKit does not solve the hostNetwork problem#

Assumed: LiveKit is "Kubernetes-native" where Jitsi is not, so it enables pod-level autoscaling via UDP mux.

Actual: LiveKit's own Kubernetes documentation states it requires host networking, and that this "limits deployments to one LiveKit pod per node." The official Helm chart defaults to podHostNetwork: true with the comment "you can run only one instance of LiveKit per physical node." Media address advertisement (rtc.use_external_ip) is architecturally identical to Jitsi's NAT_HARVESTER_PUBLIC_ADDRESS.

UDP mux is real (rtc.udp_port, conventionally 7882, multiplexing all media on one port demultiplexed by SSRC) — but enabling it does not by itself remove hostNetwork. LiveKit's docs and chart do not ship that configuration. Building it means going off the supported path.

Consequence: "LiveKit scales better on Kubernetes" is false as commonly stated. Both products have the same scaling unit — the node, not the pod.

Correction 2 — the real solution to hostNetwork is STUNner, and it is vendor-neutral#

STUNner (MIT, Kubernetes Gateway API based) exposes a single ingress port and lets media servers run in "ordinary unprivileged Kubernetes pods". It ships documented integrations for LiveKit, Jitsi, Janus, mediasoup, and others.

Consequence: STUNner is not an argument for LiveKit over Jitsi — it solves the problem for both equally. If avoiding hostNetwork is a hard requirement, STUNner is the lever, and the media-server choice becomes independent of it.

⚠️ Unverified: production maturity, performance overhead, and adoption scale. Commit authorship is near-single-maintainer. Promising lead, not a proven answer.

Correction 3 — Jitsi's P2P mode is a larger cost lever than assumed, and recording destroys it#

Jitsi ships p2p: { enabled: true } by default: with exactly two participants it attempts a direct connection, and on success media stops flowing through the bridge entirely. It keeps the bridge connection alive as a non-lossy fallback, and returns to the bridge when a third participant joins.

LiveKit has no P2P mode at all — it is a pure SFU. Every byte of every 1-to-1 call crosses your infrastructure.

For a platform whose core product is 1-to-1 consultations, this is the dominant cost term:

Jitsi (P2P succeeds) Jitsi (P2P fails) LiveKit (always)
Server media bandwidth 0 full relay full relay
Server cost per 1:1 call ~0 full full

But — and this is the finding that decides the recording question — Jibri records by joining the conference as a third participant. A conference with a third participant is no longer 1-to-1, so it falls back to the bridge. Universal recording therefore eliminates the P2P saving entirely and adds the most expensive tier. The two effects compound, and both flow from a single product decision.

OQ-08 (are consultations recorded?) is therefore not a minor product question. It is the single largest determinant of video infrastructure cost, plausibly swinging it by ~80 %. It must be answered before any video work begins.


Recording economics — where LiveKit decisively wins#

If recording is required, the comparison inverts sharply:

Jibri (Jitsi) LiveKit Egress
Concurrency "Only one recording at a time … on a single jibri" Multiple jobs per instance
Runtime Always full Chrome + X + ffmpeg, on a dedicated machine 2–6 CPUs for composited; Track egress needs no Chrome and no transcode
Cheap path None "hundreds of simultaneous TrackEgress jobs … on a single instance"
Autoscaling signal External orchestration livekit_egress_available Prometheus metric

At 300 concurrent recorded sessions, Jibri implies ~300 dedicated instances. This is the most commonly under-budgeted line item in self-hosted Jitsi, and it is the strongest single argument for LiveKit.

Other verified differentiators#

Factor Winner Detail
Component count LiveKit No XMPP stack. Jitsi needs Prosody + Jicofo + JVB + Jibri + coturn.
Embedded TURN LiveKit TURN is built into the server binary — deletes coturn entirely from the architecture
Graceful drain LiveKit Built into the server; chart sets terminationGracePeriodSeconds: 18000. Jitsi requires external orchestration.
Large rooms Jitsi Octo (bridge cascading) is open source. LiveKit's distributed mesh is Cloud-only; self-hosted caps at ~3 000/room. Irrelevant for 1-to-1.
1:1 bypass Jitsi P2P. LiveKit has none.
Signalling HA Neither Jitsi's Prosody/Jicofo are effectively singletons — the availability risk everyone overlooks while focusing on JVB
Helm chart Neither LiveKit's is first-party but ~3 months and 2 minor versions behind the server, with three meaningful commits in 18 months. Expect to fork and own it.
Licence Tie LiveKit is Apache 2.0, verified — no BSL/SSPL relicense. Egress, Ingress, and SIP are not feature-gated.

LiveKit Cloud runs the same open-source SFU — verified twice from LiveKit's own sources. What is proprietary is the orchestration layer around it (global mesh, edge routing, managed agents), not the media server.


TURN#

TURN is mandatory in production, not optional. Without TURN over TLS on 443, users on carrier-grade NAT and UDP-blocking corporate networks simply cannot connect. For a paid consultation, an unconnectable call is a refund plus a churned customer.

On relay rates — the honest position#

The commonly cited "8–20 % of sessions need TURN" figure is folklore with no traceable primary source. The one trustworthy measurement is Philipp Hancke's ~10M-call Chrome dataset: 17.7 % relayed for peer-to-peer, but only ~4 % against an SFU — and of those, 78 % were TURN/TCP, i.e. UDP-blocked networks rather than NAT symmetry.

⚠️ That data is from 2017 and desktop-Chrome-dominated. No credible published relay-rate data exists for mobile carrier networks, and none at all for Saudi Arabia or the GCC. Numbers attributed to Cloudflare in circulating articles are not on Cloudflare's pages — do not use them.

Two structural facts that cut in opposite directions: Saudi mobile networks are heavily CGNAT'd, which pushes relay rates up; but an SFU has a public IP, which makes client→server traversal far easier than client→client. Expect the binding constraint to be UDP-blocking networks, not NAT symmetry — so TURN/TLS on 443 is the highest-value connectivity investment.

Action: instrument RTCIceCandidatePair on the selected pair from day one, segmented by carrier (STC / Mobily / Zain) and access type. Within a week of real traffic you will have better data than anything published.

coturn — reputation is out of date#

coturn is actively maintained: monthly-or-better releases through 2026, current 4.14.0. The "coturn is abandoned" narrative is false as of 2026.

⚠️ But run ≥ 4.13.1. Recent high-severity CVEs include a single-packet unauthenticated remote DoS on ARM64 (CVE-2026-40613, fixed 4.10.0) and predictable nonces from LCG-based randomness (CVE-2025-69217, fixed 4.8.0). Two of the last four highs are IPv4-mapped-IPv6 ACL bypasses — if deploying dual-stack, explicitly deny ::ffff: ranges.

coturn has the same hostNetwork problem, and worse — its maintainers recommend host networking because of the 16 384-port default relay range (restrictable via min-port/max-port). Allocations are per-process; Redis shares credentials and telemetry only, not allocation state — so losing an instance drops every session on it.

This is a strong argument for LiveKit's embedded TURN: it removes an entire stateful, CVE-exposed, hostNetwork-afflicted service from the architecture.


Cost#

Egress dominates. Compute is nearly irrelevant.

Per relayed 1:1 call-hour at 1 Mbps (~0.9 GB billable egress):

Provider $/call-hr
Flat-traffic bare metal (e.g. €1/TB) ~$0.001
Cloudflare Realtime TURN ($0.05/GB) ~$0.045
Hyperscaler egress (~$0.09/GB) ~$0.081
Twilio ($0.40/GB US/EU) ~$0.36

Do not run TURN on a hyperscaler — you pay premium egress and operate the server. Worst quadrant.

Cloudflare's TURN free tier is 1 000 GB/month (~1 100 relayed call-hours), and free entirely when paired with their SFU. At launch scale this likely covers the platform outright — which sidesteps the whole coturn operational and CVE burden.

Self-host vs managed crossover: infrastructure alone is roughly break-even at ~300 concurrent. Adding a WebRTC-capable SRE — a scarce specialisation, scarcer in the KSA market — pushes the true crossover well above 300. At launch scale, self-hosting means paying the full fixed cost of a media-ops capability to serve almost no traffic.


Decision#

Abstract video behind a provider-agnostic port (SEAM-02) and defer the provider decision until OQ-08 and the residency ruling are answered.

Decision tree — do not skip step 1#

  1. Obtain a legal ruling: does transient foreign SFU relay constitute a PDPL cross-border transfer? Everything depends on this. Recordings and session metadata stay in-Kingdom regardless — they are unambiguously stored personal data and may be sensitive health data (R-04).
  2. Answer OQ-08 (recording). It swings cost ~80 % and inverts the Jitsi/LiveKit comparison.
  3. If foreign relay is acceptable → managed CPaaS at the nearest GCC point of presence, recordings written to your own in-Kingdom bucket. Fastest launch, lowest fixed cost.
  4. If foreign relay is NOT acceptable, or recording is universalself-host LiveKit on the in-Kingdom cluster (ADR-0001), with STUNner if hostNetwork must be avoided.
  5. Jitsi is the last choice — unless recording is ruled out. If consultations are not recorded, Jitsi's P2P mode makes it dramatically cheaper for a 1-to-1 product and it becomes genuinely competitive. This is the one scenario where the MoM's original proposal is correct, and it should be re-evaluated on that basis rather than dismissed.

Managed CPaaS — screening gate applied before price#

⚠️ No major CPaaS has a confirmed media presence inside Saudi Arabia. Evaluate residency before looking at pricing:

  1. Media points of presence in KSA / UAE / Bahrain?
  2. Contractual residency or geo-fencing guarantee — not merely "a nearby PoP"?
  3. Where do recordings land, and can that be pinned in-Kingdom?

Notes: Twilio Video is EOL — exclude it, and note that a major vendor exiting this exact market is the strongest available justification for the port. Amazon Chime SDK is not deprecated (the Chime application ended 2026-02-20; the SDK is actively shipping) — its server-side control plane is genuinely strong, but its React Native story is weak (no official library; only a sample you fork and own) and an OpenSSL/libvpx vulnerability issue has been open since Nov 2024. For a mobile-first product that is a material risk.


The port interface#

Provider-neutral domain concepts only. The port owns identitysessionId is a booking-domain identifier; vendor room IDs live in an adapter-owned mapping table.

createSession(bookingId, {maxParticipants, recordingPolicy, maxDurationMinutes, region?})
getSession(sessionId) / endSession(sessionId, reason) / extendSession(...)

issueAccessToken(sessionId, {userId, displayName, role, permissions, ttlSeconds})
  -> {token, connectionUrl, iceHints?}
revokeAccess(sessionId, userId)

startRecording(sessionId, {layout, audioOnly?, destination: StorageRef})
stopRecording / getRecording / listRecordings / deleteRecording

Events (normalised): ParticipantJoined/Left · SessionStarted/Ended
                     RecordingStarted/Ready/Failed
                     QualityDegraded · ConnectionFailed

Five requirements that actually determine swappability:

  1. destination: StorageRef is a compliance control, not a convenience. Recordings must land in the in-Kingdom bucket. Any vendor that cannot write to your bucket in your region fails the gate.
  2. connectionUrl returned with the token — hardcoding an endpoint client-side is the single most common thing that blocks a vendor swap. Ship this from day one even with one vendor.
  3. Reconciliation, because webhooks are lossy. Billing is on session duration; a missed SessionEnded is a billing error or an indefinitely-open paid session. Non-negotiable.
  4. ConnectionFailed / QualityDegraded are business events, not telemetry — they feed refund policy (OQ-05) and consultant-rating fairness. Most teams bolt these on after the first dispute.
  5. The mobile SDK is the leaky part. A server-side port is ~70 % of a swap; the client SDK is the other 30 % and the port cannot abstract it. Wrap the vendor SDK behind a thin mobile interface too, and budget for it explicitly.

Switch triggers — define now, while nobody is under pressure#

Trigger Action
Managed bill > ~$8–10k/mo sustained 3 months Begin self-hosted spike
Sustained concurrency > 200 Migration planning in earnest
Counsel hardens on media relay Migrate in-Kingdom immediately — keep the self-hosted path warm for this reason alone
Vendor announces EOL or repricing Exercise the port (already happened once in this market)
Measured TURN relay > ~20 % Re-run the whole cost model
Recording cost exceeds media cost Investigate track-level recording before scaling either path

Consequences#

Positive: the decision is deferred without blocking work; residency and recording rulings can arrive late; vendor exit is survivable; the port is cheap to build now.

Negative / accepted: the port is upfront work with no immediate feature value; capability negotiation is needed to degrade gracefully; the mobile SDK wrapper is an ongoing tax.


⚠ Verification gaps#

Gap Consequence
Whether any CPaaS offers KSA/GCC media presence with contractual residency Gates the entire managed option
Legal ruling on transient relay as a PDPL transfer Gates the whole decision tree — counsel, not research
STUNner production maturity and overhead Determines whether hostNetwork is avoidable
LiveKit node-failure behaviour — undocumented Rooms are pinned to one node with no replication; loss likely drops all rooms on it. Test before committing.
Real TURN relay % for KSA mobile carriers No published data exists — instrument and measure
Operational headcount for either platform No reliable data; search results were AI-generated SEO content, deliberately not cited
LiveKit embedded TURN's 1024–30000 relay range vs non-hostNetwork K8s Unresolved — reintroduces the port-range problem UDP mux was meant to solve
Tech Lead · Amir Haroun Draft v0.1 · research & design only