# ark.commentary — full documentation > Concatenated English documentation for LLM ingestion. ark.commentary is a > source-available (fair-code) self-hosted platform for remote broadcast > commentary. Canonical HTML: https://arkcommentary.ch/docs/ · curated index: https://arkcommentary.ch/llms.txt # Installation Two paths: production via docker-compose (recommended), or local dev with native services. --- ## Production (docker-compose) Prerequisites: Docker 24+, Docker Compose v2, a domain pointing to your host, ports 80/443 open + LiveKit UDP range (50000–50200/UDP, 7881/TCP) open. ```bash git clone https://github.com/<...>/commentary cd commentary cp .env.production.example .env ``` Edit `.env`: - `DOMAIN` — the public hostname (e.g. `commentary.example.com`) - `JWT_SECRET` — generate with `openssl rand -hex 64` - `ENCRYPTION_KEY` — generate with `openssl rand -hex 32` (32 bytes for AES-256-GCM) - `LIVEKIT_API_KEY` / `LIVEKIT_API_SECRET` — generate, then mirror into `infra/livekit.yaml` - `POSTGRES_PASSWORD`, `MINIO_ROOT_PASSWORD`, `REDIS_PASSWORD` — generate - `LIVEKIT_WEBHOOK_HMAC_SECRET` — generate Start the stack: ```bash docker compose up -d docker compose ps # all services healthy? docker compose logs -f backend # tail backend until "server ready" ``` Bootstrap the first admin user — see [first-admin.md](./first-admin.md). Caddy will provision Let's Encrypt TLS automatically on first request to `https://$DOMAIN/`. --- ## Local dev (native services) Prerequisites: macOS with Homebrew, Go 1.26+, Node 20+, pnpm 9+, Bun (for Astro if you touch docs later). ```bash # Backing services brew services start postgresql@17 brew services start redis brew install minio/stable/minio && minio server ~/minio-data & # DB createdb commentary_dev cd apps/server cp .env.example .env # default dev values go run ./cmd/api/migrate up # or `goose -dir migrations postgres "$DATABASE_URL" up` go run ./cmd/api # backend on :8095 # Frontends (separate terminals) cd apps/studio && pnpm install && pnpm dev # http://localhost:3030 cd apps/commentator && pnpm install && pnpm dev # http://localhost:3031 # LiveKit local livekit-server --config infra/livekit.dev.yaml & ``` The `scripts/dev.sh` script wraps all of this — `pnpm dev` from repo root brings everything up with prefixed colored logs and PID tracking. See the script for details. ### Fast end-to-end loop ```bash pnpm dev # brings up infra check + backend + studio + commentator + LiveKit pnpm seed # bootstraps a demo event + commentator + invite, prints the URLs # → click the printed invite URL to test commentator kiosk # → in studio at http://localhost:3030, use the "Dev only — Connexion rapide" # panel (DevQuickLogin component, tree-shaken in prod) to login instantly pnpm stop # clean shutdown (preserves DB/Redis volumes) ``` `scripts/seed-dev.sh` posts to the running backend API to create the demo data and prints the studio + commentator URLs ready to copy/paste. Default admin: `dev@test.ch` / `DevTest-12345!` (created by the bootstrap step ; password must be ≥ 13 chars). ### E2E tests with fake media streams (Playwright) Playwright tests for the v1.2 video flow need Chrome to grant getUserMedia without a prompt and serve synthetic video/audio. The repo's `playwright.config.ts` already sets: ```ts launchOptions: { args: ['--use-fake-ui-for-media-stream', '--use-fake-device-for-media-stream'], } ``` No additional setup required. To run only the v1.2 video spec: ```bash pnpm test:e2e --grep "live-video" ``` --- ## Studio publishing (PGM + Talkback) Since v1.6 the studio publishes PGM video + audio and Talkback intercom audio directly from the browser — no OBS, no WHIP. See [usage.md](./usage.md) section 7 for the operator workflow. ### Prerequisites - A modern Chromium-based browser on the studio machine (Chrome, Edge, Brave) for reliable `getUserMedia` device picking. Firefox works but lacks per-element sink routing. - Studio upstream bandwidth : ≥ 5 Mbps recommended (PGM 1080p30 ~3 Mbps + talkback audio + commentator subscribe). Test with `speedtest-cli` before each prod event : ```bash brew install speedtest-cli && speedtest ``` > Historical note : v1.1–v1.5 attempted OBS WHIP ingest into LiveKit. The > deployed LK v1.7.2 ships no `/whip` endpoint, so the path never worked in > prod. v1.6 dropped it in favour of in-browser publish. --- ## Next → [first-admin.md](./first-admin.md) to create your first admin account. --- # First admin bootstrap After [installation](./install.md), the database has no users. Create the first admin via the CLI shipped with the backend. ## Docker ```bash docker compose exec backend /app/commentary-admin-create \ --email you@example.com \ --password-stdin # (paste password, press Ctrl-D) ``` The CLI: 1. Hashes the password with argon2id (parameters: memory 64MB, iterations 3, parallelism 4). 2. Inserts a row in `users` with `role = 'admin'` and `email_verified = true`. 3. Does **not** auto-enroll 2FA — you do that from the studio post-login. ## Native dev ```bash cd apps/server go run ./cmd/admin-create --email you@example.com --password-stdin ``` ## Login + 2FA 1. Browse to `https:///` (or `http://localhost:3030/` in dev). 2. Log in with the credentials you just created. 3. For v1.0, the studio Settings page does **not** yet expose the 2FA enrollment wizard (planned for v1.1). To enable TOTP today, call the API directly with your access token: ```bash # Get the access token from your browser DevTools (Application → Cookies → access_token) curl -X POST https:///api/auth/2fa/setup \ -H "Cookie: access_token=$TOKEN" \ -H "Content-Type: application/json" # Returns { "secret": "...", "qr": "data:image/png;base64,..." } ``` Scan the QR with your authenticator app, then confirm with the 6-digit code: ```bash curl -X POST https:///api/auth/2fa/verify \ -H "Cookie: access_token=$TOKEN" \ -H "Content-Type: application/json" \ -d '{"code":"123456"}' # Returns { "recovery_codes": ["...", "..."] } — save these ``` Next time you log in, you'll be prompted for the TOTP code after password. ## Add more team members (Users page) Once the first admin can log in, additional admins and operators can be created via the studio UI (no more CLI needed) : 1. Navigate to `/users` (visible only to admins) 2. Click **"Ajouter"** 3. Fill email + password (≥12 chars) + role + locale 4. Save The new user can log in immediately. They can set up 2FA from Settings → Sécurité. ## Next → [usage.md](./usage.md) — operator workflow (create event, invite commentator, go live). --- # Operator Usage Guide This page covers the operator workflow once an admin is bootstrapped (see [first-admin.md](./first-admin.md)). ## 1. Login Open `https:///login` and sign in with your admin credentials. If 2FA is enabled, enter the TOTP code from your authenticator app. In development, a "Dev only — Connexion rapide" panel below the form lets you click-login as `dev@test.ch`. Tree-shaken in production. > Tip — to skip steps 2-4 below, run `pnpm seed` after `pnpm dev` : it > bootstraps a demo event + commentator + invite via the API and prints the > URLs ready to paste. Useful when you just need a working live view to > hack on. ## 2. Create an event From the events list, click **"Nouveau"**. Fill in : - **Nom** — display name (e.g., "FC Sion vs Servette") - **Description** — optional notes - **Date de début / fin** — optional scheduling The event is created in `draft` status. Click on it to open the detail view. ## 3. Add a commentator In the event detail, scroll to "Commentateurs" and click **"Ajouter"**. Fill in : - **Nom** — commentator's display name - **Langue** — fr/en/de/it - **Audio channels** — optional, used for multi-channel hardware setups - **Gain micro** — start at 0 dB v1.x only allows **one commentator per event** (mono-commentator scope). ## 4. Generate an invite link In the commentator card, expand "Invitations" and click **"Générer une invitation"**. Options : - **Mot de passe** (optionnel) — adds a password gate on the link - **Usage unique** — link burns after first use - **Expiration** — automatic expiry timestamp After generation, the **full URL** is displayed once. Click **"Copier"** to copy to clipboard, then send to the commentator (SMS, email, Telegram, etc). ⚠️ The URL is shown only once — copy it before closing the modal. ## 5. Transition event status Available transitions : - `draft` → `scheduled` (planning confirmed) - `scheduled` → `live` (commentator about to connect) - `live` → `ended` (broadcast complete) - `ended` → `archived` (long-term storage) Click the buttons in the event detail. Invalid transitions are hidden. ## 6. Go live (studio side) Click **"Démarrer en direct"** to open the live view : - LiveKit token is minted server-side, studio joins the commentator's room - Audio meter animates at 60Hz - On-air badge updates in real-time via WebSocket when the commentator clicks ON-AIR New in v1.2 — two additional video panels appear once the commentator connects: - **ComCam** appears as a video panel beside the PGM preview when the commentator publishes their webcam. Use the eye icon to hide it locally if it distracts from the audio meter. If the commentator cuts their cam, you see the placeholder. - **PGM preview** shows what the commentator sees, audio muted to avoid feedback. ## 7. PGM + Talkback in-browser publish (v1.6+) The studio publishes PGM video+audio and Talkback intercom audio **directly from the browser** — no OBS, no external streamer. In the live view, two panels appear under the commentator card : - **PGM** — pick a webcam + a mic, click "Publier". The commentator sees the video full-screen behind the on-air controls. Audio is routed to the `pgm-audio` track and is mixed into the commentator's foldback. - **Talkback** — pick the intercom mic (typically a closed-back headset or the régie talkback handset), click "Publier". Audio goes to the `talkback-audio` track. The commentator hears it on a separate channel with the talkback PiP placeholder visible in the corner of the kiosk. The publish state is per-tab. If the operator closes the studio tab, both tracks unpublish — the commentator drops back to silence. > Historical note : v1.1–v1.5 routed PGM via OBS WHIP. The deployed LiveKit > v1.7.2 lacks the `/whip` endpoint, so the path never worked in prod. > v1.6 replaced WHIP with in-browser publish. ## 8. Revoke / rotate invites In the commentator card, expand "Invitations". For each active invite : - **Révoquer** — burns the invite immediately. Useful if the link was leaked. To rotate (= revoke + new), revoke the existing one then click "Générer une invitation" again. ## 9. Manage users (admin only) From the events list header, admins see a **"Utilisateurs"** link. On the users page : - **Ajouter** — create a new operator or admin - **Modifier** — change role, display name, locale - **Désactiver** — soft-delete (user can't login, audit log preserved) You cannot disable yourself. ## 10. Settings - **Thème** — light/dark toggle (persisted in localStorage) - **Langue** — fr/en/de - **Sécurité** — configure 2FA, change password After password change you're logged out (all sessions revoked). --- # Troubleshooting Common issues and how to diagnose them. ## `/readyz` returns 503 The backend reports unhealthy. Check what's failing: ```bash docker compose ps docker compose logs backend | tail -50 docker compose exec backend wget -qO- http://localhost:8095/readyz ``` `/readyz` probes Postgres + Redis + LiveKit. The JSON response lists which dependency is down. Usually: - Postgres not ready yet (wait 10–20s after `docker compose up`) - Redis password mismatch between `.env` and the container - LiveKit signaling unreachable (check `LIVEKIT_HOST` env var resolves) ## LiveKit connection refused / WebSocket fails ```bash docker compose logs livekit | tail -30 ``` Two common causes: 1. **API key/secret mismatch.** `infra/livekit.yaml` has `keys: { : }` and the backend reads `LIVEKIT_API_KEY` / `LIVEKIT_API_SECRET` from `.env`. They must match exactly. Regenerate both sides if unsure. 2. **UDP ports blocked.** WebRTC media flows over UDP 50000–50200 (configurable in `infra/livekit.yaml`). On a firewalled host these must be open. TCP fallback uses 7881 — slower but works through restrictive networks. ## Cookie not set after login Symptom: login API returns 200 OK but the browser immediately bounces back to `/login`. The session cookie wasn't accepted. Causes: - `DOMAIN` env var doesn't match the host you're hitting (cookie domain mismatch) - You're accessing via HTTP, not HTTPS — `Secure` cookies require TLS in modern browsers - You're behind a proxy that strips `Set-Cookie` — check Caddy logs ```bash docker compose logs caddy | grep -i cookie ``` ## 401 Unauthorized on protected routes Access tokens have a 15-minute TTL. On expiry the studio auto-refreshes silently on the next request via the opaque refresh token (30 days sliding). If you see 401s in burst: - Refresh token also expired → user must re-login - Clock skew between client and server > 30s → fix NTP on the host - Refresh token revoked (logged out elsewhere) → re-login ## Postgres migration fails ```bash docker compose exec backend goose -dir /app/migrations \ postgres "$DATABASE_URL" status ``` If goose is out of sync (a migration was applied manually, or a previous run crashed mid-migration), you can: ```bash # See the state goose -dir migrations postgres "$DATABASE_URL" status # Force to a known version (CAREFUL — does not run the SQL) goose -dir migrations postgres "$DATABASE_URL" version goose -dir migrations postgres "$DATABASE_URL" up-by-one ``` For a fully botched dev DB, drop and recreate: ```bash docker compose down postgres docker volume rm commentary_postgres-data docker compose up -d postgres backend ``` ## Commentator kiosk shows "no permission" for microphone Browser blocked mic access. The kiosk is Chrome-only (uses `AudioContext.sinkId` for output device routing). In Chrome: 1. Click the lock icon in the address bar 2. Site settings → Microphone → Allow 3. Reload the page On macOS / Windows also check OS-level mic permission for Chrome. ## Caddy can't get TLS cert ```bash docker compose logs caddy | grep -i acme ``` Typical issues: - Port 80 blocked or already used by another service (Caddy needs :80 for HTTP-01) - DNS hasn't propagated yet — wait 5 minutes then `docker compose restart caddy` - Rate-limit hit on Let's Encrypt (5 certs per domain per week) — wait or use the staging endpoint via `acme_ca https://acme-staging-v02.api.letsencrypt.org/directory` in the Caddyfile temporarily ## Still stuck? Open an issue with the output of: ```bash docker compose ps docker compose logs --tail=100 backend caddy livekit ``` Scrub any secrets before posting. --- # License FAQ ark.commentary is fair-code: the source is public, self-hosting for your own use is free, and commercial exploitation for third parties is what funds the project. ## Can my TV station / production company use it for free? Yes. Deploy it on your own infrastructure and use it for your own productions — including commercial broadcasts. The Sustainable Use License permits free internal business use without restriction. ## Can I modify it? Yes, for your own use. Patches welcome upstream (see CONTRIBUTING.md). ## Can I deploy it for my clients, resell hosting, or white-label it? That requires the Enterprise License. Contact licensing@ark.swiss — integrators and resellers are exactly who it exists for. ## Is it open source? It is source-available under the fair-code model (like n8n). The full source is public and auditable, but the Sustainable Use License is not an OSI-approved open source license: it restricts commercial exploitation for third parties. ## Why not AGPL/MIT? A copyleft or permissive license would let integrators resell deployments of ark.commentary without sustaining its development. The fair-code boundary keeps self-hosting free while funding the project through the people who build businesses on it. --- # Changelog User-facing highlights per release. ## v2.15 — June 2026 - Released as fair-code under the **Sustainable Use License**, with a separate Enterprise license for integrators and resellers. ## v2.14 - Encrypted daily backups for production deployments. - Beta instance scaffolding and an LTS branch split. ## v2.13 - **Hybrid talkback**: hold to talk, quick tap to latch. - Real-time presence and on-air state hardened — WebSocket keepalive, one-time Redis ticket for the upgrade. - Live PGM video stats on the kiosk: resolution, fps, Mbps. ## v2.12 - In-browser PGM video capture with simulcast. ## v2.11 - Opus bitrate applied live, with no audio cut. ## v2.9–2.10 - Studio control as a device-keyed lease: read-only by default, consented takeover, a single active session. - Kiosk seat flow over the invite link: claim, heartbeat, takeover request. - Automatic microphone recovery on the commentator side, with a banner on hard failure. ## v2.7–2.8 - Redesigned studio events: live presence counters, an event detail cockpit, a status stepper. - Quick invite actions with a QR code. ## v2.0–2.6 - v2 foundation: rebuilt studio and kiosk, receiver-side audio telemetry (jitter, NetEq), EBU PPM metering.