# Architecture Documentation for Linear Void
Linear Void architecture is documented using the [arc42 template](https://arc42.org/).
> Companion docs: see [Documentation README](../README.md) and `adr/` for full decision rationale, alternatives, and consequences.
- [Architecture Documentation for Linear Void](#architecture-documentation-for-linear-void)
- [Introduction and Goals](#introduction-and-goals)
- [Architecture Constraints](#architecture-constraints)
- [Context and Scope](#context-and-scope)
- [Solution Strategy](#solution-strategy)
- [Building Block View](#building-block-view)
- [Runtime View](#runtime-view)
- [Deployment View](#deployment-view)
- [Cross-cutting Concepts](#cross-cutting-concepts)
- [Architecture Decisions](#architecture-decisions)
- [Quality Requirements](#quality-requirements)
- [Risks and Technical Debts](#risks-and-technical-debts)
- [Glossary](#glossary)
## Introduction and Goals
**Requirements Overview** — Linear Void is an independent **24/7 linear video channel** broadcasting self-owned pre-recorded videos to a primarily US audience via a custom lightweight HTML/CSS/JS SPA with synchronized HLS playback, schedule metadata, public chat rooms, and private 1:1 messaging. Fully independent of third-party video platforms (YouTube, Twitch, etc.).
**Core Requirements** (merged from all ADRs):
- Continuous synchronized playlist playback (live-style HLS, not VOD).
- Self-hosted source media, frontend, schedule, and chat data.
- Primary US audience/origin (East Coast preferred for low latency/peering).
- Extreme cost efficiency (near-zero egress via Cloudflare R2 + edge caching).
- Custom chat with rooms, DMs, presence, moderation, persistence.
- Storage for 48h of 720p30/1080p30 content.
- Browser-compatible playback (`hls.js` + native HLS).
- Operational simplicity and scalability.
Full rationale in [`adr/ADR-001.md`](adr/ADR-001.md).
**Quality Goals**
| # | Attribute | Goal | Scenario |
| --- | ------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------ |
| 1 | Cost Efficiency | <$25/month at moderate scale (100s concurrent) | R2 $0 egress + immutable `.m4s`/`.ts` edge caching removes per-viewer bandwidth cost |
| 2 | Performance/Latency | Low US latency; acceptable HLS delay | Hetzner Ashburn origin + Cloudflare US POPs (15–35s end-to-end) |
| 3 | Availability | 24/7 with minimal downtime | FFmpeg/MediaMTX auto-restart, fallback slate, monitoring |
| 4 | Scalability | Grow 10s→1000s viewers without proportional cost | Edge caching + R2; only request volume limits |
| 5 | Maintainability | Lightweight, clear configs, low ops | Static SPA, Go/Node chat, SQLite/PostgreSQL+Redis |
| 6 | Security/Usability | Secure transport, moderation, good UX | HTTPS/WSS, Cloudflare WAF, rate limits, intuitive chat/player |
**Stakeholders**: Product Owner, Engineering Team, US Viewers, Moderators/Admins, Operations — all expecting a cost-effective, reliable, low-latency, low-TCO independent channel with rich chat and easy management.
## Architecture Constraints
- No YouTube/Twitch/paid Cloudflare Stream dependency.
- US-centric (origin + audience).
- Cost stays low even at scale (bandwidth dominant).
- Standard browser playback only (no plugins).
- Cloudflare ToS compliance for video (R2 as origin for large immutable files).
- HLS primary (standard, not LL-HLS initially).
- Self-hosted playlist normalization and continuous playout.
## Context and Scope
**Business Context** — Linear Void runs a continuous scheduled, playlist-driven program to a US audience. The custom viewer site replaces traditional platforms, giving full control over content, schedule, UI, and real-time chat (public rooms + DMs).
**High-Level Context Diagram**
```mermaid
flowchart TD
subgraph "External Users"
Viewers[US Viewers
Browser SPA + hls.js + Chat UI]
Admins[Admins / Moderators
Playlist & Schedule UI]
end
subgraph "Cloudflare Global Edge"
CDN[Cloudflare CDN
Cache Rules + WAF + TLS]
Workers[Cloudflare Workers
Static SPA Hosting]
R2[Cloudflare R2
HLS Segments + Source MP4s
enam location hint]
end
subgraph "US-East Origin (Hetzner Ashburn VPS)"
VPS[VPS CX22
Nginx Proxy + Monitoring]
Playout[Playout Service
FFmpeg + MediaMTX/SRS
Continuous HLS from Playlist]
Chat[Chat Backend
WebSockets + Redis + DB
Public rooms + Private DMs]
end
Admins -->|HTTPS - Schedule/Playlist| Playout
Playout -->|S3-compatible Upload| R2
R2 -->|Cached HLS| CDN
Viewers -->|HTTPS| Workers
Viewers -->|HLS Requests| CDN
Viewers -->|WSS Chat| Chat
Chat --> VPS
VPS -->|Proxy / Static| Workers
CDN -->|Origin Pull on Miss| R2
```
**External Interfaces**: Cloudflare (CDN cache rules for `.m4s`/`.ts` vs `.m3u8`, R2 $0 egress, Workers static hosting, WAF/TLS); Hetzner VPS (US-East compute, good peering); Browsers (native HLS + WebSocket); Source (pre-normalized MP4s in R2).
**Technical Context**
- **Playout**: FFmpeg consuming normalized playlist → continuous HLS.
- **Packaging/Delivery**: MediaMTX/SRS/Nginx → immutable `.m4s`/`.ts` to R2; edge cache.
- **Frontend**: Static SPA with hls.js.
- **Chat**: Lightweight WebSocket service + Redis (presence/rate limits) + PostgreSQL/SQLite (history/moderation).
- **Normalization**: All sources H.264/AAC, constant 30 fps, 2s keyframes.
**Mapping**: Input = normalized MP4s in R2 + playlist; Output = live `master.m3u8` + segments via Cloudflare, chat via WSS.
## Solution Strategy
1. **US-East Playout on Hetzner Ashburn VPS** (CX22+): FFmpeg + MediaMTX/SRS continuous playlist→HLS (`-c copy` where possible).
2. **Immutable HLS to Cloudflare R2**: unique segment names; R2 origin with $0 egress.
3. **Aggressive Edge Caching**: long TTL for immutable `.m4s`/`.ts`; short (1–10s) for live `.m3u8`; Tiered Cache enabled — viewer delivery effectively free.
4. **Lightweight Custom Frontend**: static SPA + hls.js + chat sidebar.
5. **Separate co-located Chat Service**: WebSockets with persistence/moderation (low bandwidth).
6. **Normalization & Monitoring**: pre-process all videos; dashboards + alerts for playout health, cache hit ratio, R2 ops.
**Accepted Trade-off**: standard HLS latency (15–35s) acceptable for linear programming/chat; LL-HLS/WebRTC deferred.
## Building Block View
**Whitebox Overall System**
```mermaid
graph TD
subgraph Cloudflare
CDN[CDN + Cache Rules
.m4s and .ts immutable long TTL
.m3u8 short TTL]
R2[R2 - HLS + Source]
Workers[Workers - Static SPA]
end
VPS[VPS Hetzner Ashburn]
Playout[FFmpeg + MediaMTX/SRS
Playlist → HLS]
Chat[WebSocket Chat
Node/Go + Redis + DB]
Viewer[US Viewers]
Admin[Admin UI]
Admin --> Playout
Playout --> R2
R2 --> CDN
Viewer --> Workers
Viewer --> CDN
Viewer --> Chat
Chat --> VPS
VPS --> Nginx[Nginx Proxy]
```
**Motivation**: decouple playout (continuous linear), delivery (cached immutable segments), interaction (chat). R2 + edge caching solves bandwidth cost while keeping synchronization and independence.
**Contained blocks**: Playout (FFmpeg + MediaMTX/SRS); Storage/Delivery (R2 + CDN); Frontend (SPA); Chat Service (WebSocket); Proxy/Static (Nginx).
**Important interfaces**: HTTP(S) for HLS, WSS for chat, S3-compatible for R2 uploads.
**Level 2 — components with code locations and persistence**
```mermaid
graph TB
subgraph "Cloudflare Edge"
CF[CDN + WAF
Cache Rules]
R2[R2 Bucket
media + HLS]
WORKERS[Workers
SPA hosting]
end
subgraph "Docker Host (Hetzner CX22)"
NGINX[nginx
:80/:443
proxy + auth]
CHAT[chat service
:3000
Go + gorilla/websocket]
PLAYOUT[playout service
:8081
Go + FFmpeg]
REDIS[Redis 7
:6379
presence/rate/pubsub]
DB[PostgreSQL 15
:5432
chat history/moderation]
end
subgraph "External"
VIEWER[Viewer Browser
hls.js + WebSocket]
ADMIN[Admin Browser
SPA + uploads]
end
VIEWER -->|HTTPS| CF
VIEWER -->|WSS| NGINX
ADMIN -->|HTTPS| CF
CF --> NGINX
NGINX --> CHAT
NGINX --> PLAYOUT
NGINX -->|/hls/*| R2
CHAT --> REDIS
CHAT --> DB
PLAYOUT --> R2
PLAYOUT -->|reads| R2
```
**Playout Service** (`backend/playout/src/playlist.go`)
- Role: continuous HLS encoding loop via FFmpeg concat demuxer.
- Input: `data/playlist/playlist.txt` (ordered MP4 paths); Output: HLS segments (`.m4s`, `.ts`) + live `.m3u8` to `data/hls/`, mirrored to R2.
- Resilience: restart counter (>5/60s → slate); Prometheus on 8081.
- Validation: ffprobe gate (codec/fps/resolution/keyframe); `-validate` mode checks playlist without running.
- Seamless looping: resets HLS only when playlist fingerprint (SHA-256) changes, crash/slate recovery, or first run; else continues via `-start_number`.
- R2 retention: `cleanupOldRuns` keeps newest `R2_KEEP_RUNS` (default 2) capped at `R2_MAX_RETENTION_BYTES` (default 10 GiB), after each run + hourly backstop.
- Shared config: reads runtime `config.json` (`loadSharedHLSConfig`) as env fallback for HLS segment duration/window.
**Chat Service** (`backend/chat/src/main.go`)
- Role: WebSocket chat (rooms, DMs, moderation, admin API).
- Persistence: PostgreSQL 15 via GORM (messages, moderation, media metadata); SQLite dev fallback.
- Redis: presence, rate limiting, pub/sub multi-instance fan-out.
- Auth: session-cookie admin auth, Redis-backed expiry.
- Metrics on `/metrics`: connected clients, stored messages, DMs, moderation actions.
**Frontend SPA** (`frontend/`)
- Viewer `src/main.ts`: hls.js player, WS chat, presence, schedule overlay.
- Admin `src/admin.ts`: 9 tabs (dashboard, upload, playlist, schedule, moderation, chat-log, users, analytics, system).
- Build: Vite → `frontend/dist/`.
**Reverse Proxy** (`infrastructure/nginx/nginx.conf`)
- `auth_request` gate on `/admin` and `/api/admin`; WS upgrade for `/ws`; HLS CORS + OPTIONS preflight; `/health` JSON + `/nginx-status` stub_status.
**Level 3 — Chat Service Modules**
| Module | Location | Responsibility |
| ------------------ | ------------------------------------------------------- | -------------------------------------------------------------------------- |
| Connection Manager | `main.go` (hub, clients map) | Track active WS connections per room, fan-out |
| Message Parser | `main.go` (readPump, handleMessage) | Parse JSON envelope `{type, room?, ...}`, validate, route |
| Room Registry | `main.go` (rooms map) | Room name → client set, join/leave/close |
| DM Router | `main.go` (handleDirectMessage) | Resolve user, deliver private message |
| Persistence Layer | `main.go` (GORM: Message, DirectMessage, ModerationLog) | Store/retrieve messages, DMs, moderation |
| Redis Subsystem | `main.go` (redisClient) | INCR+EXPIRE rate limit, SETEX presence, PSUBSCRIBE fan-out |
| Auth Handler | `main.go` (handleAdminLogin, adminAuthMiddleware) | Session cookie create/validate, login rate limit |
| User Auth Handler | `handlers/handlers.go` | Viewer register/login/logout/session (bcrypt, user cookie) |
| DM History/Search | `handlers/handlers.go` | Paginated DM history + search |
| Runtime Config | `config/runtime.go` | Thread-safe JSON runtime config (HLS, Matomo) |
| Admin API Router | `main.go` (handleAdmin\*) | Upload, playlist CRUD, schedule overrides, moderation, analytics, system |
| Metrics Exporter | `main.go` | connected_clients, messages_total, dms_total, moderation_actions, redis_up |
**Level 3 — Playout Service Modules**
| Module | Location | Responsibility |
| ---------------- | ------------------------------------------------------- | --------------------------------------------------------------------------------- |
| Playlist Parser | `playlist.go` (parsePlaylist) | Read playlist, skip comments/blanks, resolve paths |
| FFmpeg Manager | `playlist.go` (runPlayout) | Spawn/monitor FFmpeg concat, restart on crash |
| Loop Controller | `playlist.go` (playlistFingerprint, hlsNextStartNumber) | Detect changes (SHA-256), continue numbering |
| Slate Controller | `playlist.go` (activateSlate/deactivateSlate) | Switch to slate on excessive failures |
| R2 Sync | `playlist.go` (syncToR2) | Upload segments + manifest to R2 |
| R2 Cleanup | `playlist.go` (cleanupOldRuns) | Keep last N run folders under size cap; delete `hls/r-*/*` |
| Health Endpoint | `playlist.go` (healthHandler) | `/health` JSON: mode, segment count, uptime |
| Metrics Exporter | `playlist.go` | restarts_total, mode, loops_total, run_duration_seconds, r2_cleanup_objects_total |
**Level 3 — Frontend Modules**
| Module | File | Responsibility |
| ------------------- | ----------------------------------------------- | ------------------------------------------------------------------- |
| HLS Player | `src/main.ts` | hls.js init, attach, error recovery |
| WebSocket Client | `src/main.ts` | Connect/parse envelope/dispatch |
| Chat UI | `src/main.ts` | Message list, room tabs, DM panel, presence dots, typing, user-list |
| DM Search | `src/main.ts` | `/api/dm/search` + render |
| Auth UI | `src/main.ts` + `index.html` | Viewer login/register/logout, session, user label |
| Schedule Overlay | `src/main.ts` | Poll `/api/schedule`, render current/next |
| Admin Router | `src/admin.ts` | Tab nav, dynamic pages |
| Upload Manager | `src/admin.ts` | Multipart chunk upload, normalization badges |
| Playlist Editor | `src/admin.ts` | Drag-reorder (gated to `ready` media), save |
| Moderation Panel | `src/admin.ts` + `public/admin/moderation.html` | Mute/ban, action log |
| Analytics Dashboard | `src/admin.ts` + `public/admin/analytics.html` | Message counts, top rooms, moderation stats |
| System Config | `src/admin.ts` + `public/admin/system.html` | Health, runtime config form, restart |
## Runtime View
**Playout & Delivery Scenario**
1. Admin adds normalized MP4 to R2 + updates playlist.
2. Playout worker (FFmpeg concat loop) reads next file, packages HLS segments (2s).
3. Segments uploaded to R2 with unique immutable names; live `.m3u8` updated.
4. Viewer loads SPA → requests manifest → hls.js follows current edge.
5. Cloudflare serves `.m4s`/`.ts` from nearest edge cache (origin hit once per POP/region).
6. Parallel WebSocket to chat service for synchronized interaction.
Manifest freshness keeps all viewers on the same timeline; cache hit ratio drives cost/latency. Fallback slate activates on failure. (Moderation events, scale spikes, failover in ADR.)
## Deployment View
**CI/CD Pipeline** — Parallel workflows on each push to `main`: **CI builds/pushes images** while **CD deploys** to the test lab (`192.168.88.40`); a dedicated release workflow handles semantic version tags. Production Hetzner VPS planned but not yet provisioned — same pipeline deploys there by changing `DOCKER_HOST`.
```mermaid
flowchart LR
subgraph "Gitea Actions"
CI[CI Workflow
Build + Push Images]
CD[CD Workflow
Pull + Deploy]
end
subgraph "Gitea Container Registry"
REGISTRY[git.allucanget.biz
playout:latest
chat:latest
nginx:latest]
end
subgraph "Test Lab Host (192.168.88.40) — Production Hetzner TBD"
DOCKER[Docker Engine
via TCP :2375]
PLAYOUT[linearvoid-playout]
CHAT[linearvoid-chat]
NGINX[linearvoid-nginx]
REDIS[linearvoid-redis]
DB[linearvoid-db]
PROM[linearvoid-prometheus]
GRAF[linearvoid-grafana]
end
CI -->|push| REGISTRY
REGISTRY -->|pull| CD
CD -->|deploy via DOCKER_HOST| DOCKER
DOCKER --> PLAYOUT
DOCKER --> CHAT
DOCKER --> NGINX
DOCKER --> REDIS
DOCKER --> DB
DOCKER --> PROM
DOCKER --> GRAF
```
**Pipeline sequence**
1. **CI** (`.gitea/workflows/ci.yml`): on `push` to `main` + `pull_request`; `test` job (frontend lint/build, Go validation); `docker` job (after test, main only) builds `playout`/`chat`/`nginx` with Buildx, pushes `main` + `latest` tags to Gitea registry.
2. **CD** (`.gitea/workflows/cd.yml`): on `push` to `main` (parallel) or `workflow_dispatch`; logs into remote Docker host, generates `.env` from secrets, seeds Prometheus/Grafana volumes, deploys via `docker compose -f docker-compose.prod.yml up -d --remove-orphans --force-recreate` to `192.168.88.40:2375` (switch to Hetzner IP for prod); health checks with retry for playout init.
3. **Release** (`.gitea/workflows/release.yml`): on tags `*.*.*` (e.g. `1.0.9`); pushes `X.Y.Z`, `X.Y`, `latest`; creates Gitea release.
**Infrastructure Level 1** — Single low-cost US-East origin (Hetzner) with global edge caching. High cache-hit ratio, low US latency, edge-scale WAF/DDoS. Mapping: Playout + Chat + Nginx → Hetzner CX22 (Ashburn, VA); HLS storage/caching/static SPA → Cloudflare R2 + CDN + Workers (`enam` location hint); all public traffic through Cloudflare.
**Infrastructure Level 2**
- **Hetzner VPS (planned)**: playout (FFmpeg/MediaMTX), chat backend, Nginx; monitoring dashboards + UptimeRobot; backups to R2. Test lab currently fills this role (see `DEPLOYMENT.md`).
- **Cloudflare**: R2 bucket; Cache Rules (`.m4s`/`.ts` Cache Everything/immutable/long TTL; `.m3u8` short TTL/must-revalidate); WAF, rate limiting, Tiered Cache; Workers for SPA.
- Future: Docker Compose, managed DB, secondary regions (later ADRs).
## Cross-cutting Concepts
**Caching & Cost Optimization** — Immutable segment naming + targeted Cloudflare Cache Rules + R2 ($0 egress) = near-zero viewer bandwidth. Long TTL on `.m4s`/`.ts`, short on live manifests. Monitor hit ratio and R2 Class B ops.
**Video Normalization & Playout** — All sources must meet the Linear Void source profile before production playlist acceptance. The chat backend **auto-normalizes** non-conforming uploads on ingest (`media.NormalizeVideo`), removing manual pre-processing. Prevents decode resets, timestamp drift, AV discontinuities during concat playout.
_Required Source Profile (v1)_
| Field | Required Value | Notes |
| ----------------- | --------------------------- | ------------------------------------------------ |
| Container | MP4 | Single-video-asset input |
| Video codec | H.264/AVC (`libx264`) | Baseline delivery codec |
| Audio codec | AAC-LC | Stereo preferred |
| Resolution | 1280x720 or 1920x1080 | Keep single res per playlist block when possible |
| Framerate | Constant 30 fps (`-r 30`) | VFR must be normalized |
| GOP/keyframe | 2s (`-g 60 -keyint_min 60`) | Matches HLS segment boundaries |
| Pixel format | `yuv420p` | Browser baseline |
| Audio sample rate | 48 kHz | Stable concat timing |
| Audio bitrate | 128k–192k | Predictable envelope |
_Reference normalization command_
```bash
ffmpeg -i input.mov \
-c:v libx264 -preset medium -crf 21 \
-r 30 -g 60 -keyint_min 60 -sc_threshold 0 \
-pix_fmt yuv420p \
-c:a aac -b:a 160k -ar 48000 -ac 2 \
-movflags +faststart \
output-normalized.mp4
```
_Automatic normalization & verification (ffprobe)_
- `media.NormalizeVideo` runs ffmpeg with the profile, writes `*.normalized.mp4`, re-verifies with ffprobe.
- Media status: `ready`, `needs_normalization`, `normalizing`, `normalization_failed`; admin UI "Normalize" action with polling.
- `POST /api/admin/media/{id}/normalize` re-triggers manually.
- Keep verification aligned with runtime playout assumptions to preserve `-c copy` efficiency.
**Runtime Configuration Store** — Thread-safe, JSON-persisted store in `backend/chat/src/config/runtime.go`, shared with playout via the playlist volume (`/app/playlist/config.json`).
- Editable via `GET/PUT /api/admin/config`.
- Fields: `hlsSegmentDuration` (2–15s, default 2), `hlsWindowSize` (3–100, default 12), `matomoUrl`, `matomoSiteId`.
- Playout reads same file (`loadSharedHLSConfig`) as env fallback.
- Typed setters validate; invalid → `400`.
**Analytics (Matomo)** — Viewer sends pageviews via public `GET /api/matomo`, returning `matomoUrl` + `matomoSiteId` from the runtime config. Optional per deployment (empty `matomoUrl` disables).
**Monitoring & Resilience** — Two-layer model: (1) application layer (chat + platform health) via Prometheus + lightweight Grafana; (2) infrastructure/runtime layer (Prometheus self-observability, TSDB, Go runtime) via detailed Grafana dashboard.
_Prometheus baseline_ (`monitoring/prometheus/prometheus.yml`): 15s scrape; 7 jobs — `chat`→`chat:3000`, `playout`→`playout:8081`, `nginx`→`nginx-exporter:9113`, `redis`→`redis-exporter:9121`, `postgres`→`postgres-exporter:9187`, `node`→`node-exporter:9100`, `prometheus`→`localhost:9090`. TSDB retention `30d` + `15GB` disk cap (both compose files).
_Grafana dashboards_: `monitoring/grafana/dashboard.json` (app metrics: connected clients, messages, moderation, Redis reachability, admin sessions, scrape health); `monitoring/grafana/dashboards/dashboard.json` (Prometheus ops: status, uptime, CPU/mem, TSDB ingestion/churn/errors, HTTP latencies, query load, GC/scheduler). Use app dashboard for service behavior; Prometheus overview for platform diagnostics/capacity.
_Alerting rules_ (`monitoring/prometheus/alerts.yml`, loaded via `rule_files`): 10 rules — `ChatDown`, `PlayoutDown`, `RedisDown`, `PlayoutSlateActive`, `HighChatLatency`, `PrometheusWALFailing`, `HighCPU`, `LowCacheHitRatio`, `HighRedisMemoryUsage`, `LowDiskSpace`. Route warning/critical to ops channel; attach runbook links.
_Resilience baseline_: emergency slate fallback; auto-restart for playout/chat/nginx; weekly backup/restore drills for stateful data; incident runbook triage: edge reachability → origin health → playout progression → chat connectivity → data-plane dependencies.
**API Reference** — Three operational API surfaces (canonical contract in `API.md`).
_Chat WebSocket API_: endpoint `/ws`; `wss://` prod / `ws://` local; query params `username`, `room`; envelope `{room, username, text, type: public|private|system}`; responsibilities — room fan-out, DM routing, moderation commands, rate limiting/presence.
_Playout & Delivery REST_: playout `/health`; edge/origin `/health` via nginx; HLS manifest/segment namespace under `/hls/`. Liveness for orchestration + stable HLS path contract.
_Admin REST_: auth/session `/api/admin/login|logout|session`; domains overview/analytics/users/moderation/schedule/playlist/media; runtime config `GET/PUT /api/admin/config`; normalization `POST /api/admin/media/{id}/normalize`; uploads `/api/admin/media/uploads` (init/resume), `/uploads/{uploadId}/chunk`, `/complete`. Protected control plane; media metadata + upload orchestration; moderation/audit + system status.
_Viewer REST_: auth `POST /api/auth/register|login|logout`, `GET /api/auth/session`; DMs `GET /api/dm/history` (paginated), `GET /api/dm/search`; analytics `GET /api/matomo` (public).
**Cloudflare Configuration Reference** — Cloudflare is part of the production control plane, not only a CDN.
_DNS & zone routing_: `linearvoid.tv` → app origin (nginx); `www.linearvoid.tv` → redirect apex; `media.linearvoid.tv` → media domain for HLS/object delivery when split-domain enabled.
_Cache rules baseline_: `.ts`/`.m4s` Cache Everything, long TTL, immutable; `.m3u8` short TTL (1–10s), revalidation-friendly.
_Edge security/controls_: enforce HTTPS/WSS + redirect HTTP; WAF baseline; rate limits on abusive patterns (auth/admin); minimize origin exposure.
_Worker & header policy_: the `linearvoid-media` Worker ([`infrastructure/cloudflare/`](../../infrastructure/cloudflare/), R2-bound) owns `media.linearvoid.tv` and enforces immutable caching for segments, short TTL for live manifests, plus HLS CORS/Range; the `linearvoid-frontend` Worker ([`frontend/wrangler.toml`](../../frontend/wrangler.toml)) serves the static SPA on `linearvoid.tv`. Automated deploys: `workers-deploy.yml` (SPA) and `workers-media-deploy.yml` (media).
_Operations & validation_: verify cache status progression (MISS→HIT), manifest freshness under active playout; track hit ratio, request/error rates, ToS/cost posture.
**Security & Moderation** — Split across edge (Cloudflare), proxy (nginx), and application (chat/admin).
- _Transport/edge_: HTTPS/WSS; WAF + rate limiting; Worker enforces immutable headers.
- _nginx_: `auth_request` on `/admin/`, `/api/admin/`; admin UI → redirect `/admin/login/`; admin API → JSON `401`; WS upgrade forwarded for `/ws`.
- _Admin auth_: cookie session (`linearvoid_admin_session`); `HttpOnly`, `SameSite=Strict`, `Secure` when HTTPS (from TLS or `X-Forwarded-Proto`); server-side expiry.
- _Viewer accounts_: register/login/logout; bcrypt passwords; `linearvoid_user_session` cookie, 30-day default TTL (`USER_SESSION_TTL_HOURS`); `SameSite=None; Secure` over HTTPS (cross-site from Workers SPA), `SameSite=Lax` over plain HTTP (local dev); sign-in UI in `frontend/index.html`.
- _Rate limiting/abuse_: Redis `INCR + EXPIRE`; baseline `>10` messages in `10s` → throttle. Redis down → presence/rate-limit degrade gracefully (warn logged; core chat available).
- _CORS_: HLS endpoints `Access-Control-Allow-Origin: *`; API routes reflect allowlisted origin + `Access-Control-Allow-Credentials: true`; viewer cookie `SameSite=None; Secure` cross-site; admin cookie stays `SameSite=Strict`.
- _Moderation/privacy_: commands `/mod`, `/mute`, `/ban` handled in chat flow, persisted. DMs are application-level private, **not** end-to-end encrypted. Formalize moderation/audit retention in runbook/ops docs for production hardening.
**Chat** — WebSockets + Redis (presence, pub/sub, rate limits) + DB (history, audit). Public rooms + on-demand private chats; scales independently of video. DM history/search persisted; viewer UI has typing indicator, user-list toggle, DM search. (Details/cache tables/bitrate/cost model/configs in ADR-001.)
## Architecture Decisions
Consolidated ADRs (in `adr/`):
- **ADR-001**: overall architecture, HLS + R2 caching, storage calcs, cost model, risks.
- **ADR-002**: project folder/module structure and rationale.
- **ADR-003**: tech stack (TypeScript frontend; Go for chat/playout wrapper; Bash/Python scripts), Docker-first, documented assumptions.
- **ADR-004**: video admin web interface (upload, playlist, schedule overrides, normalization).
- **ADR-005**: admin UI for users, moderation dashboard, analytics, system config.
- **ADR-006**: data storage (SQLite vs PostgreSQL for v1, migration path).
- **ADR-007**: Redis usage (use cases, TTL, persistence, fallback, admin config).
**Key decisions**: self-managed US-East playout (Hetzner Ashburn) FFmpeg + MediaMTX/SRS → HLS → R2 + edge cache; module structure mirroring arc42 blocks; Go for perf-critical services, TypeScript frontend, Docker everywhere; admin interfaces (HTMX + custom CSS); SQLite primary for v1 with PostgreSQL migration path; Redis for rate limiting/presence/pub/sub/session with defined TTL and fallback.
## Quality Requirements
See **Quality Goals**. Primary: cost (near-zero egress), availability (auto-restart + fallback), scalability (edge caching), usability (synchronized player + responsive chat).
**Performance Benchmarks & Capacity Planning**
_Baseline SLO targets (v1)_: playback startup < 4s after first manifest request on warm cache; no segment starvation in steady state; end-to-end HLS 15–35s; chat delivery p95 < 500ms same region; admin read APIs p95 < 1s.
_Workload assumptions_: US audience, US-East origin; edge serves most segments, origin handles manifest churn + cache misses; chat low-bandwidth vs video.
_Initial capacity model_: bitrates — 720p30 ~2.6–4.2 Mbps, 1080p30 ~5.1–8.2 Mbps (with audio). 48h library — 720p30 ~57–90 GB raw (alloc ~80–110 GB); 1080p30 ~110–175 GB raw (alloc ~130–180 GB). Cost driven by cache efficiency + request volume, not direct origin egress.
_Benchmark plan_: playback (time-to-first-frame, rebuffers across US regions; manifest freshness/segment availability); chat (concurrent WS + fan-out; p50/p95 latency, Redis fallback); admin/API (p95 latency, error rate); platform (TSDB ingestion, query p99, GC in Grafana).
_Capacity triggers/actions_: viewer growth + rising cache-miss → tighten cache, enable/verify Tiered Cache, review manifest TTL; chat p95 drift/WS errors → scale chat, tune Redis, isolate moderation; Prometheus WAL/query saturation → tune retention/cardinality, add resources, split queries; origin CPU/mem pressure → optimize FFmpeg, isolate services, add secondary origin.
**Scaling Strategy** — preserve cost efficiency first, then throughput/resilience in phases:
- **Phase 1 — Vertical + Cache-First (current)**: single US-East origin, strong edge caching; optimize segment immutability + manifest TTL before adding infra; scale compute vertically on pressure.
- **Phase 2 — Service Separation**: split playout/chat/observability across nodes; isolate noisy workloads from latency-sensitive chat; introduce managed PostgreSQL/Redis or dedicated state nodes where justified.
- **Phase 3 — Multi-Origin Resilience**: warm standby or active-secondary origin; health-aware DNS/origin failover; keep HLS/cache contract stable.
- **Phase 4 — Regional/Throughput Expansion**: add origin region only when audience/latency justify; explicit chat affinity strategy; evaluate cache-miss economics vs origin complexity.
- _Scaling triggers_: rising cache-miss with sustained growth; chat p95/p99 drift or WS errors; persistent origin saturation; increased blast-radius from single-host dependency.
**Disaster Recovery & Backup**
_Recovery targets_: RPO ≤ 24h (chat/moderation/admin metadata); RTO ≤ 60 min (restore baseline streaming + chat).
_Backup scope_: persistent volumes — Postgres (`postgres-data`), Redis append-only (`redis-data`) where enabled, Prometheus TSDB (`prometheus-data`) if retention required, Grafana state (`grafana-data`); host-mounted dirs — `data/chat`, `data/playlist`, `data/media` (authoritative may be R2), `data/hls` (optional, regenerable); source of truth — compose, nginx, monitoring configs, env template.
_Cadence_: daily logical Postgres backups (+ weekly full snapshot); daily sync of chat/playlist dirs; weekly restore integrity check in isolated env. Retention: daily 14 days, weekly 8 weeks, monthly 6 months.
_Recovery runbook (minimum)_:
1. Restore infra config (compose, nginx, env, monitoring).
2. Restore Postgres + Redis state.
3. Restore `data/playlist` + chat persistence dirs.
4. Rehydrate media from authoritative object storage if missing.
5. Start stack; validate in order: playout `/health`, chat `/health`, nginx `/health`, HLS availability, WS chat connectivity.
6. Verify admin auth + moderation audit visibility.
7. Announce recovery; elevated monitoring ≥ one playlist cycle.
_Failure modes_: single-host loss → redeploy on standby, restore backups, repoint DNS/edge; storage corruption → restore last good snapshot + replay logical backups; partial failure → replace only impacted service, restore scoped data. Rehearse quarterly.
## Risks and Technical Debts
- Cloudflare ToS risk for heavy video on free tier (use R2; fallback to BunnyCDN).
- HLS latency (15–35s) may affect perceived chat sync.
- Single VPS = SPOF (mitigate with DNS failover, Phase 3).
- Upfront effort for normalization + cache tuning.
- Moderation/abuse operational load at scale.
- R2 request costs on cache misses (optimize Tiered Cache; measure in prod).
- Deferred: LL-HLS, ABR, E2EE, paid access controls, full Docker Compose.
- _Debt_: detailed service configs, CI/CD, advanced resilience left for implementation ADRs.
## Glossary
| Term | Definition |
| ----------------- | -------------------------------------------------------------------------------- |
| HLS | HTTP Live Streaming — `.m3u8` manifests + media segments (fMP4). |
| Immutable Segment | HLS chunk with unique, unchanging name enabling long-lived caching. |
| R2 | Cloudflare object storage ($0 egress; ideal origin for cached HLS). |
| Edge Cache / POP | Cloudflare points-of-presence caching/serving content near viewers. |
| Playout | Continuous generation of a scheduled linear program from a playlist. |
| Normalization | Pre-processing to consistent codec/fps/keyframe settings for seamless playback. |
| Cache Rule | Cloudflare config for caching behavior, TTL, headers per URL pattern. |
| SPOF | Single Point of Failure (current single VPS origin). |
| Tiered Cache | Cloudflare feature reducing origin requests by checking upper-tier caches first. |
**References**: Cloudflare R2/Cache docs, MediaMTX/SRS/FFmpeg HLS guides, Hetzner pricing, hls.js docs, video bitrate calculators. Original root `ADR-001-*.md` files consolidated and can be archived/deleted.