# kuploy native stack spec — LiveKit (video layer for live classes)
# Self-contained: a *provision*-mode import creates the service, generates the
# LIVEKIT_KEYS pair, and assigns a domain — no pre-created services needed.
#
# Single-node by design: a one-replica LiveKit server runs on its in-memory bus
# and needs NO redis. Redis is only required for a DISTRIBUTED/multi-node setup
# (shared room routing + message bus) — config-sample.yaml: "when redis is set,
# LiveKit will automatically operate in a fully distributed fashion". If you
# scale livekit-server past one replica, add a redis component and set
# REDIS_HOST: "<host>:6379" (single combined host:port — note there is NO
# LIVEKIT_REDIS_HOST/PORT var). Verified against livekit/livekit, 2026-06-24.
#
# LIVEKIT_KEYS is "key: secret" (kuploy's keypair generator emits this shape).
#
# ⚠️ WebRTC/UDP: LiveKit needs UDP for media. Signaling (7880/TCP) goes through
# normal Ingress, but media does NOT — expose the RTC ports out-of-band:
#   7880/TCP   signaling (HTTP API + WebSocket)            REQUIRED (ingress)
#   7881/TCP   ICE/TCP, UDP fallback for restricted nets   REQUIRED
#   7882/UDP   ICE/UDP media (rtc.udp_port, single mux)    REQUIRED for media
#   3478/UDP   embedded TURN/UDP                           optional
#   5349/TCP   TURN/TLS relay                              optional, recommended
# This template runs the SINGLE-PORT UDP mux (rtc.udp_port: 7882) instead of the
# default 50000-60000 range — the always-on prerequisite from kuploy-k8s#30 that
# collapses media exposure to one UDP port (a 10k-port range can't map onto a
# Service LoadBalancer; one port can). Verified 2026-06-26 to carry real media
# end-to-end (livekit-cli load-test: video publisher + subscribers, 0% packet
# loss) over this single port, and the SFU advertises a 7881/TCP ICE candidate
# for the UDP-blocked fallback.
# K8s note: this template is ClusterIP-only (serviceType: application, no
# hostNetwork). When the shared STUNner TURN gateway is installed (kuploy-k8s#30),
# the kuploy server AUTO-WIRES media at deploy time: it injects the TURN block
# into LIVEKIT_CONFIG (pointing at the gateway's live address + static credential)
# and creates a per-tenant UDPRoute targeting this component's ClusterIP Service.
# Nothing to configure here — the udp_port mux below is the signal the server
# keys off. If STUNner is NOT installed, the app still deploys with working
# signaling but media won't route until you expose 7882/UDP out-of-band
# (hostNetwork/hostPort for single-node) or install STUNner.
# If UDP is unreachable end-to-end you MUST expose 7881/TCP or media fails.

name: livekit
description: LiveKit SFU for live teaching (video + screen-share)
components:
  - name: livekit-server
    type: web
    serviceType: application
    domain: true
    port: 7880 # WebSocket signaling; RTC/UDP needs separate exposure (see above)
    source:
      type: docker
      image: livekit/livekit-server:v1.13.1 # pinned (do not use :latest)
    env:
      - key: LIVEKIT_KEYS # "key: secret" — generated at provision time
        generate: keypair
      # Single-port UDP mux + external-IP advertisement (kuploy-k8s#30 "always"
      # prerequisite). udp_port replaces the 50000-60000 range with one port;
      # use_external_ip makes the SFU advertise its reachable IP via STUN so ICE
      # succeeds behind NAT/ingress. The presence of rtc.udp_port is what flags
      # this component as WebRTC media: when STUNner is installed the server
      # merges a turn_servers entry into this value at deploy time (live gateway
      # address + static credential) — do NOT hardcode a turn block here.
      #
      # ⚠️ MUST be single-line flow YAML, NOT a `value: |` block. kuploy stores a
      # service's env as a newline-delimited KEY=VALUE string and re-parses it by
      # splitting on "\n" (orchestrator parseEnvVars), so any newline inside a
      # value truncates it at the first line — a block scalar here reaches the
      # container as just `LIVEKIT_CONFIG=rtc:` and the mux silently doesn't apply
      # (server falls back to rtc.portICERange 50000-60000). Flow style has no
      # newlines and round-trips intact; LiveKit parses it as YAML all the same.
      - key: LIVEKIT_CONFIG # flow YAML; merged with LIVEKIT_KEYS from the env above
        value: "{rtc: {tcp_port: 7881, udp_port: 7882, use_external_ip: true}}"
