52 KiB
Architecture Documentation for Linear Void
The architecture documentation for the Linear Void project is structured according to the arc42 template. This template provides a comprehensive framework for documenting software architecture, ensuring that all relevant aspects of the system are covered.
Table of Contents
- Architecture Documentation for Linear Void
- Table of Contents
- Companion Documentation
- Introduction and Goals
- Architecture Constraints
- Context and Scope
- Solution Strategy
- Building Block View
- Runtime View
- Deployment View
- Cross-cutting Concepts
- Architecture Decisions
- Quality Requirements
- Risks and Technical Debts
- Glossary
Companion Documentation
see Documentation README for an overview of all documentation, including architecture, ADRs, operational procedures, and setup guides.
Introduction and Goals
Requirements Overview
Linear Void is an independent 24/7 linear video channel broadcasting a playlist of self-owned pre-recorded videos. Viewers access it via a custom lightweight HTML/CSS/JavaScript SPA with synchronized HLS playback, schedule metadata, public chat rooms, and private 1:1 messaging. Complete independence from third-party video platforms (YouTube, Twitch, etc.).
Core Requirements (merged from all ADRs):
- Continuous synchronized playback from playlist (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 (bandwidth dominant; target near-zero egress via Cloudflare R2 + edge caching).
- Custom chat with rooms, DMs, presence, moderation, persistence.
- Storage planning for 48h of 720p30/1080p30 content.
- Browser-compatible playback (
hls.js+ native HLS). - Operational simplicity and scalability.
Full decision rationale, alternatives, and consequences in adr/ADR-001.md.
Quality Goals
| Priority | Quality Attribute | Goal | Scenario |
|---|---|---|---|
| 1 | Cost Efficiency | <$25/month for moderate scale (100s concurrent) | R2 $0 egress + immutable .m4s and .ts edge caching eliminates 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 loop, fallback slate, monitoring |
| 4 | Scalability | Grow from 10s to 1000s viewers without proportional cost | Edge caching + R2; only request volume limits |
| 5 | Maintainability | Lightweight, clear configs, low ops overhead | Static SPA, Go/Node chat, SQLite/PostgreSQL+Redis |
| 6 | Security / Usability | Secure transport, moderation tools, good UX | HTTPS/WSS, Cloudflare WAF, rate limits, intuitive chat/player |
Stakeholders
| Role/Name | Contact | Expectations |
|---|---|---|
| Product Owner | - | Cost-effective independent 24/7 channel with synchronized viewing and rich chat |
| Engineering Team | - | Simple, evolvable stack (FFmpeg, HLS, Cloudflare R2, lightweight WebSocket chat) |
| US Viewers | - | Reliable low-latency playback, responsive synchronized chat experience |
| Moderators / Admins | - | Easy playlist management, moderation dashboard, monitoring/alerting |
| Operations | - | Low TCO, clear cost model, automated failover, backups |
(See adr/ADR-001.md for detailed forces, decision drivers, and rationale.)
Architecture Constraints
- Independence from video hosting platforms (no YouTube, Twitch, paid Cloudflare Stream).
- US-centric (origin + audience).
- Cost must remain low even at scale (bandwidth dominant).
- Standard browser playback only (no plugins).
- Cloudflare ToS compliance for video delivery (use R2 as origin for large immutable files).
- HLS as primary protocol (standard, not LL-HLS initially).
- Self-hosted playlist normalization and continuous playout required.
(See adr/ADR-001.md for full forces and trade-offs.)
Context and Scope
Business Context
Linear Void provides a continuously running scheduled video program (playlist-driven) to a primarily US audience. The custom viewer site replaces traditional platforms, giving full control over content, schedule, UI, and real-time chat (public rooms + private DMs).
High-Level Context Diagram (Mermaid)
flowchart TD
subgraph "External Users"
Viewers[US Viewers<br/>Browser SPA + hls.js + Chat UI]
Admins[Admins / Moderators<br/>Playlist & Schedule UI]
end
subgraph "Cloudflare Global Edge"
CDN[Cloudflare CDN<br/>Cache Rules + WAF + TLS]
Pages[Cloudflare Pages<br/>Static SPA Hosting]
R2[Cloudflare R2<br/>HLS Segments + Source MP4s<br/>enam location hint]
end
subgraph "US-East Origin (Hetzner Ashburn VPS)"
VPS[VPS CX22<br/>Nginx Proxy + Monitoring]
Playout[Playout Service<br/>FFmpeg + MediaMTX/SRS<br/>Continuous HLS from Playlist]
Chat[Chat Backend<br/>WebSockets + Redis + DB<br/>Public rooms + Private DMs]
end
Admins -->|HTTPS - Schedule/Playlist| Playout
Playout -->|S3-compatible Upload| R2
R2 -->|Cached HLS| CDN
Viewers -->|HTTPS| Pages
Viewers -->|HLS Requests| CDN
Viewers -->|WSS Chat| Chat
Chat --> VPS
VPS -->|Proxy / Static| Pages
CDN -->|Origin Pull on Miss| R2
External Interfaces:
- Cloudflare: CDN (cache rules for
.m4sand.tsvs.m3u8), R2 (object storage, $0 egress), Pages (static hosting), WAF/TLS. - Hetzner VPS: Compute for playout and chat (low-cost US-East with good peering).
- Browsers: Native HLS (
<video>+ hls.js) and WebSocket APIs. - Source: Pre-normalized MP4 files uploaded to R2.
This diagram synthesizes the context views from all original ADRs. Full decision details, cache rules, storage calculations, and rationale are in adr/ADR-001.md.
Technical Context
- Playout: FFmpeg consuming normalized playlist → continuous HLS.
- Packaging/Delivery: MediaMTX/SRS/Nginx → immutable
.m4sand.tssegments to R2; Cloudflare edge cache. - Frontend: Static SPA with hls.js.
- Chat: Lightweight WebSocket service (Node/Go) + Redis (presence/rate limits) + PostgreSQL/SQLite (history/moderation).
- Normalization: All source videos must be H.264/AAC, 30 fps constant, 2s keyframes.
Mapping:
- Input: Normalized MP4s in R2 + playlist.
- Output: Live
master.m3u8+ segments served via Cloudflare; chat via WSS.
Solution Strategy
Core Strategy (synthesized & accepted from all ADRs):
- US-East Playout on Hetzner Ashburn VPS (
CX22or equivalent): FFmpeg + MediaMTX/SRS for continuous playlist-to-HLS conversion (-c copywhere possible). - Immutable HLS to Cloudflare R2: Segments use unique names; R2 acts as origin with $0 egress.
- Aggressive Edge Caching: Long TTL for
.m4sand.ts(immutable), short TTL (1–10s) for live.m3u8. Tiered Cache enabled. This makes viewer delivery essentially free. - Lightweight Custom Frontend: Static HTML/JS SPA + hls.js player + embedded chat sidebar.
- Separate but co-located Chat Service: WebSockets with persistence and moderation (low bandwidth).
- Normalization & Monitoring: Pre-process all videos; use dashboards + alerts for playout health, cache hit ratio, R2 ops.
This eliminates egress economics, ensures synchronization via live playlist, keeps ops simple, and scales well. See adr/ADR-001.md for detailed cache rules, storage tables, cost model, configs (FFmpeg/Nginx examples), and phase-by-phase implementation plan.
Key Trade-off Accepted: Standard HLS latency (15–35s) is acceptable for linear programming/chat. LL-HLS/WebRTC deferred.
Building Block View
Whitebox Overall System
Overview (Mermaid diagram):
graph TD
subgraph Cloudflare
CDN[CDN + Cache Rules<br/>.m4s and .ts immutable long TTL<br/>.m3u8 short TTL]
R2[R2 - HLS + Source]
Pages[Pages - Static SPA]
end
VPS[VPS Hetzner Ashburn]
Playout[FFmpeg + MediaMTX/SRS<br/>Playlist → HLS]
Chat[WebSocket Chat<br/>Node/Go + Redis + DB]
Viewer[US Viewers]
Admin[Admin UI]
Admin --> Playout
Playout --> R2
R2 --> CDN
Viewer --> Pages
Viewer --> CDN
Viewer --> Chat
Chat --> VPS
VPS --> Nginx[Nginx Proxy]
Motivation: Decouple playout (continuous linear), delivery (cached immutable segments), and interaction (chat). Cloudflare R2 + edge caching solves the bandwidth cost problem while maintaining synchronization and independence.
Contained Building Blocks (high-level):
- Playout: FFmpeg + MediaMTX/SRS (continuous HLS from playlist).
- Storage/Delivery: Cloudflare R2 + CDN (immutable segments).
- Frontend: Static SPA (hls.js player + chat UI).
- Chat Service: WebSocket backend with persistence/moderation.
- Proxy/Static: Nginx on VPS.
Important Interfaces: HTTP(S) for HLS, WSS for chat, S3-compatible for R2 uploads.
Detailed black-box descriptions, level-2/3 views, and code locations are in adr/ADR-001.md and future implementation ADRs.
Level 2
Detailed component diagram with code locations and persistence.
graph TB
subgraph "Cloudflare Edge"
CF[CDN + WAF<br/>Cache Rules]
R2[R2 Bucket<br/>media + HLS]
PAGES[Pages<br/>SPA hosting]
end
subgraph "Docker Host (Hetzner CX22)"
NGINX[nginx<br/>:80/:443<br/>proxy + auth]
CHAT[chat service<br/>:3000<br/>Go + gorilla/websocket]
PLAYOUT[playout service<br/>:8081<br/>Go + FFmpeg]
REDIS[Redis 7<br/>:6379<br/>presence/rate/pubsub]
DB[PostgreSQL 15<br/>:5432<br/>chat history/moderation]
end
subgraph "External"
VIEWER[Viewer Browser<br/>hls.js + WebSocket]
ADMIN[Admin Browser<br/>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 using FFmpeg concat demuxer
- Input:
data/playlist/playlist.txt— ordered list of MP4 file paths - Output: HLS segments (
.m4sand.ts) + live manifest (.m3u8) written todata/hls/, mirrored to R2 - Resilience: Restart counter (>5/60s → activate slate), Prometheus metrics on port 8081
- Validation: ffprobe gate on all ingested files (codec, fps, resolution, keyframe interval);
-validatemode checks the playlist without running the loop - Seamless looping: Only resets HLS output when the playlist fingerprint (SHA-256) changes, on crash/slate recovery, or first run; otherwise continues segment numbering via
-start_number - R2 retention:
cleanupOldSegmentsdeleteshls/r-*/*objects older thanR2_SEGMENT_RETENTION_HOURS(default 24h) hourly + at startup - Shared config: Reads the runtime
config.json(loadSharedHLSConfig) as an env fallback for HLS segment duration/window
Chat Service (backend/chat/src/main.go)
- Role: WebSocket chat with rooms, DMs, moderation, admin API
- Persistence: PostgreSQL 15 via GORM (messages, moderation logs, media metadata). SQLite for dev fallback
- Redis: Presence tracking, rate limiting, pub/sub for multi-instance fan-out
- Auth: Session-cookie admin auth with Redis-backed expiry
- Metrics: Connected clients, stored messages, DMs, moderation actions on
/metrics
Frontend SPA (frontend/)
- Viewer:
src/main.ts— hls.js player, WebSocket chat, presence indicators, schedule overlay - Admin:
src/admin.ts— 9 dynamic tabs (dashboard, upload, playlist, schedule, moderation, chat-log, users, analytics, system) - Build: Vite → static output in
frontend/dist/
Reverse Proxy (infrastructure/nginx/nginx.conf)
- auth_request gate on
/adminand/api/admin - WebSocket upgrade for
/ws - HLS CORS headers + OPTIONS preflight
/healthJSON endpoint,/nginx-statusstub_status
Level 3
Module-level breakdown per service.
Chat Service Modules
| Module | File/Lines | Responsibility |
|---|---|---|
| Connection Manager | main.go (hub, clients map) |
Track active WebSocket connections per room, fan-out messages |
| Message Parser | main.go (readPump, handleMessage) |
Parse JSON envelope {type, room?, ...}, validate fields, route to handler |
| Room Registry | main.go (rooms map, room broadcasts) |
Map room name → client set, handle join/leave/close |
| DM Router | main.go (handleDirectMessage) |
Resolve target user in clients map, deliver private message |
| Persistence Layer | main.go (GORM models: Message, DirectMessage, ModerationLog) |
Store/retrieve chat messages, DMs, moderation events |
| Redis Subsystem | main.go (redisClient, rate limiting, pub/sub, presence) |
INCR+EXPIRE rate limit, SETEX presence, PSUBSCRIBE room fan-out |
| Auth Handler | main.go (handleAdminLogin, adminAuthMiddleware) |
Session cookie creation/validation, login rate limiting |
| User Auth Handler | handlers/handlers.go (HandleUserRegister/Login/Session/Logout) |
Viewer register/login/logout/session (bcrypt, user session cookie) |
| DM History/Search | handlers/handlers.go (HandleUserDMHistory, HandleUserDMSearch) |
Paginated DM history + search for authenticated users |
| Runtime Config | config/runtime.go (GetRuntime, SetRuntime, typed setters) |
Thread-safe JSON runtime config (HLS duration/window, Matomo) |
| Admin API Router | main.go (handleAdmin*) |
Upload, playlist CRUD, schedule overrides, moderation actions, analytics, system summary |
| Metrics Exporter | main.go (prometheus.MustRegister) |
Expose connected_clients, messages_total, dms_total, moderation_actions, redis_up |
Playout Service Modules
| Module | File/Lines | Responsibility |
|---|---|---|
| Playlist Parser | playlist.go (parsePlaylist) |
Read playlist.txt, skip comments/blank lines, resolve file paths |
| FFmpeg Manager | playlist.go (runPlayout, restart logic) |
Spawn/monitor FFmpeg concat process, restart on crash |
| Loop Controller | playlist.go (playlistFingerprint, hlsNextStartNumber) |
Detect playlist changes (SHA-256), continue segment numbering on clean loops |
| Slate Controller | playlist.go (activateSlate, deactivateSlate) |
Switch output to slate loop on excessive failures |
| R2 Sync | playlist.go (syncToR2) |
Upload HLS segments + manifest to R2 bucket |
| R2 Cleanup | playlist.go (cleanupOldSegments) |
Delete stale hls/r-*/* segments older than the retention window |
| Health Endpoint | playlist.go (healthHandler) |
/health → JSON with mode, segment count, uptime |
| Metrics Exporter | playlist.go (prometheus) |
Expose restarts_total, mode (live/slate), loops_total, run_duration_seconds, r2_cleanup_objects_total |
Frontend Modules
| Module | File | Responsibility |
|---|---|---|
| HLS Player | src/main.ts |
hls.js initialization, attach to video element, error recovery |
| WebSocket Client | src/main.ts |
Connect/disconnect, parse envelope, dispatch to UI handlers |
| Chat UI | src/main.ts |
Message list rendering, room tabs, DM panel, presence dots, typing indicator, user-list toggle |
| DM Search | src/main.ts |
Search DM history (/api/dm/search) and render results |
| Auth UI | src/main.ts + index.html |
Viewer login/register/logout, session refresh, user label |
| Schedule Overlay | src/main.ts |
Poll /api/schedule, render current/next program |
| Admin Router | src/admin.ts |
Tab-based navigation, dynamic page loading |
| Upload Manager | src/admin.ts |
Multipart upload with chunk progress, normalization status badges |
| Playlist Editor | src/admin.ts |
Drag-reorder, add/remove items (gated to ready media), save to /api/admin/playlist |
| Moderation Panel | src/admin.ts + public/admin/moderation.html |
Inline mute/ban, action log viewer |
| Analytics Dashboard | src/admin.ts + public/admin/analytics.html |
Message counts, top rooms, moderation stats |
| System Config | src/admin.ts + public/admin/system.html |
Service health, runtime config form (HLS duration/window, Matomo), restart buttons |
Runtime View
Playout & Delivery Scenario
- Admin adds normalized MP4 to R2 + updates playlist.
- Playout worker (FFmpeg loop with concat demuxer) reads next file, packages as HLS segments (2s).
- Segments uploaded to R2 with unique immutable names; live
.m3u8updated. - Viewer loads SPA → requests master/live manifest → hls.js player follows current edge.
- Cloudflare serves
.m4sand.tsfrom nearest edge cache (origin/R2 hit only once per POP/region). - Parallel WebSocket connection to chat service for synchronized interaction.
Notable aspects: Manifest freshness ensures all viewers stay on same timeline; cache hit ratio directly impacts cost and latency. Fallback slate activates on playout failure. Full cache policy, example configs (FFmpeg bash loop, Nginx RTMP HLS), and storage tables in adr/ADR-001.md.
(Additional scenarios: moderation events, scale spikes, failover covered in the ADR.)
Deployment View
CI/CD Pipeline Architecture
The deployment uses parallel workflows on each push to main: CI builds and pushes images while CD deploys latest images to the test lab. A dedicated release workflow handles semantic version tags. Current runtime target is the test lab (192.168.88.40). Production Hetzner VPS is planned but not yet provisioned — the same pipeline will deploy there by changing DOCKER_HOST.
flowchart LR
subgraph "Gitea Actions"
CI[CI Workflow<br/>Build + Push Images]
CD[CD Workflow<br/>Pull + Deploy]
end
subgraph "Gitea Container Registry"
REGISTRY[git.allucanget.biz<br/>playout:latest<br/>chat:latest<br/>nginx:latest]
end
subgraph "Test Lab Host (192.168.88.40) — Production Hetzner TBD"
DOCKER[Docker Engine<br/>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:
- CI Workflow (
.gitea/workflows/ci.yml):- Triggers on
pushtomainandpull_request - Runs
testjob: frontend lint/build and Go validation - Runs
dockerjob (aftertestpasses, only onmainpush):- Builds
playout,chat,nginximages with Buildx - Pushes to Gitea Container Registry with
mainandlatesttags
- Builds
- Triggers on
- CD Workflow (
.gitea/workflows/cd.yml):- Triggers on
pushtomain(parallel to CI), or manually viaworkflow_dispatch - Logs in remote Docker host to private registry
- Generates
.envfrom secrets - Seeds Prometheus and Grafana provisioning into named volumes
- Deploys via
docker compose -f docker-compose.prod.yml up -d --remove-orphans --force-recreateto remote host (192.168.88.40:2375— test lab; switch to Hetzner IP for production) - Runs health checks with retry logic for playout initialization
- Triggers on
- Release Workflow (
.gitea/workflows/release.yml):- Triggers on semantic tags in
*.*.*format (for example1.0.9) - Builds and pushes versioned image tags (
X.Y.Z,X.Y,latest) - Creates release entry in Gitea
- Triggers on semantic tags in
Infrastructure Level 1
Overview Diagram: See Mermaid in Building Block section (VPS + Cloudflare R2/Pages/CDN).
Motivation: Low-cost US-East origin with global edge caching. Hetzner provides excellent price/performance and included traffic; Cloudflare eliminates egress fees.
Quality/Performance Features: High cache-hit ratio for video, low US latency, automatic scaling at edge, built-in WAF/DDoS protection.
Mapping of Building Blocks to Infrastructure:
- Playout + Chat + Nginx → Hetzner Cloud CX22 (Ashburn, VA).
- HLS storage, caching, static SPA → Cloudflare R2 + CDN + Pages (custom domains,
enamlocation hint). - All public traffic routed through Cloudflare.
Infrastructure Level 2
Hetzner VPS (planned): Will run playout (FFmpeg/MediaMTX), chat backend, Nginx. Monitoring via dashboards + UptimeRobot. Backups to R2. Currently test lab (192.168.88.40) serves this role. Production Hetzner instance not yet provisioned — see Production Deployment section in DEPLOYMENT.md.
Cloudflare Layer:
- R2 bucket for media.
- Cache Rules (
.m4sand.ts: Cache Everything, immutable, long TTL;.m3u8: short TTL/must-revalidate). - WAF, rate limiting, Tiered Cache.
- Pages for SPA hosting.
Future evolution: Docker Compose, managed DB, secondary regions (detailed in subsequent ADRs).
Cross-cutting Concepts
Caching & Cost Optimization
Immutable segment naming + targeted Cloudflare Cache Rules + R2 ($0 egress) = near-zero viewer bandwidth cost. Long TTL on .m4s and .ts, short on live manifests. Monitor hit ratio and R2 Class B ops.
Video Normalization & Playout
All source assets must conform to the Linear Void source profile before they are accepted into production playlists. The chat backend auto-normalizes non-conforming uploads on ingest (media.NormalizeVideo), so admins no longer need to pre-process files manually. This prevents decode resets, timestamp drift, and audio/video discontinuities during continuous concat playout.
Required Source Profile (Linear Void v1)
| Field | Required Value | Notes |
|---|---|---|
| Container | MP4 | Single-video-asset input format |
| Video codec | H.264/AVC (libx264) |
Baseline delivery codec for broad compatibility |
| Audio codec | AAC-LC | Stereo preferred |
| Resolution | 1280x720 or 1920x1080 | Keep single resolution per playlist block when possible |
| Framerate | Constant 30 fps (-r 30) |
Variable frame rate content must be normalized |
| GOP / keyframe interval | 2 seconds (-g 60 -keyint_min 60) |
Matches HLS segment boundaries better |
| Pixel format | yuv420p |
Browser compatibility baseline |
| Audio sample rate | 48 kHz | Stable timing for concat playout |
| Audio bitrate | 128k-192k | Predictable quality envelope |
Reference Normalization Command
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.NormalizeVideoruns ffmpeg with the Linear Void profile (H.264/AAC, 30fps, 2s keyframes, yuv420p, 48kHz stereo), writes*.normalized.mp4, and re-verifies the output with ffprobe.- Media records carry a status:
ready,needs_normalization,normalizing, ornormalization_failed; the admin UI shows a "Normalize" action with status polling. POST /api/admin/media/{id}/normalizere-triggers normalization manually for failed/re-queued media.- Keep verification logic aligned with runtime playout assumptions to preserve
-c copyefficiency where possible.
Runtime Configuration Store
A thread-safe, JSON-persisted runtime config store lives in backend/chat/src/config/runtime.go and is shared with the playout service via the playlist volume (/app/playlist/config.json).
- Editable via
GET/PUT /api/admin/configfrom the admin UI. - Fields:
hlsSegmentDuration(2–15s, default 6),hlsWindowSize(3–100, default 24),matomoUrl,matomoSiteId. - Playout reads the same file (
loadSharedHLSConfig) as an env fallback for HLS segment duration/window. - Typed setters validate values; invalid input is rejected with
400.
Analytics (Matomo)
- The viewer sends pageviews to Matomo driven by the public
GET /api/matomoendpoint, which returnsmatomoUrl+matomoSiteIdfrom the runtime config store. - Analytics is optional per deployment: leave
matomoUrlempty to disable tracking.
Monitoring & Resilience
Linear Void uses a two-layer monitoring model:
- Application layer (chat + platform health) via Prometheus metrics and a lightweight Grafana dashboard.
- Infrastructure/runtime layer (Prometheus self-observability, TSDB, query engine, Go runtime) via a detailed Grafana dashboard.
Prometheus Baseline
- Config file:
monitoring/prometheus/prometheus.yml - Default scrape interval:
15s - Current scrape jobs (7 total):
chat->chat:3000playout->playout:8081nginx->nginx-exporter:9113redis->redis-exporter:9121postgres->postgres-exporter:9187node->node-exporter:9100prometheus->localhost:9090
- TSDB retention:
--storage.tsdb.retention.time=30d(delete metrics older than 30 days) +--storage.tsdb.retention.size=15GB(disk cap), set on theprometheusservice indocker-compose.ymlanddocker-compose.prod.yml
Grafana Dashboards
monitoring/grafana/dashboard.json(Linear Void app metrics: connected clients, messages, moderation actions, Redis reachability, admin sessions, scrape health).monitoring/grafana/dashboards/dashboard.json(Prometheus operational visibility: status, uptime, CPU/memory, TSDB ingestion/churn/errors, HTTP latencies, query load, GC/scheduler).
Use the app dashboard for service-level behavior and the Prometheus overview dashboard for platform diagnostics and capacity planning.
Alerting Rules (Prometheus Rule Groups)
Rules are implemented in monitoring/prometheus/alerts.yml and loaded via rule_files from prometheus.yml.
Current rule set (10):
ChatDownPlayoutDownRedisDownPlayoutSlateActiveHighChatLatencyPrometheusWALFailingHighCPULowCacheHitRatioHighRedisMemoryUsageLowDiskSpace
Route warning/critical severities to the operations channel and keep runbook links attached to each alert definition.
Operational resilience baseline:
- Emergency slate fallback for playout failure.
- Auto-restart policies for playout/chat/nginx containers.
- Weekly backup and restore drills for stateful data (
data/chat, playlist metadata, Postgres). - Incident runbook with triage order: edge reachability -> origin health -> playout progression -> chat connectivity -> data-plane dependencies.
API Documentation Reference
Linear Void currently exposes three operational API surfaces. The canonical detailed contract should live in API.md, while this section provides the architecture-level map.
Chat WebSocket API
- Endpoint:
/ws - Transport:
wss://in production,ws://locally - Query parameters:
usernameroom
- Message envelope (typical):
roomusernametexttype(public,private,system)
- Responsibilities:
- room fan-out
- private DM channel routing
- moderation command handling
- rate limiting and presence integration
Playout and Delivery REST API
- Health endpoints:
- playout health (
/healthon playout service) - edge/origin health (
/healthvia nginx)
- playout health (
- Stream delivery:
- HLS manifest and segment path namespace under
/hls/
- HLS manifest and segment path namespace under
- Responsibilities:
- expose liveness for orchestration and monitoring
- provide stable HLS path contract for viewer and CDN
Admin REST API
- Auth/session:
/api/admin/login/api/admin/logout/api/admin/session
- Operational/admin domains:
- overview, analytics, users, moderation, schedule, playlist, media
- runtime config:
GET/PUT /api/admin/config(HLS segment duration/window, Matomo) - media normalization:
POST /api/admin/media/{id}/normalize
- Media upload domain:
/api/admin/media/uploads(init/resume)/api/admin/media/uploads/{uploadId}/chunk/api/admin/media/uploads/{uploadId}/complete
- Responsibilities:
- protected control plane for operator workflows
- media metadata + upload orchestration
- moderation/audit and system status management
Viewer REST API
- Auth/session:
POST /api/auth/registerPOST /api/auth/loginPOST /api/auth/logoutGET /api/auth/session
- Private messaging:
GET /api/dm/history(paginated)GET /api/dm/search
- Analytics:
GET /api/matomo(public; returns Matomo URL + site ID for the viewer pageview snippet)
Cloudflare Configuration Reference
Cloudflare is part of the production control plane, not only a CDN edge. The following configuration set is required to maintain cost, latency, and reliability targets.
DNS and Zone Routing
linearvoid.tv-> application origin (nginx)www.linearvoid.tv-> redirect to apexmedia.linearvoid.tv-> media domain for HLS/object delivery when split-domain mode is enabled
Cache Rules Baseline
- HLS segments (
.ts,.m4s):- Cache Everything
- long TTL (immutable)
- respect immutable semantics
- Manifests (
.m3u8):- short TTL (1-10s)
- revalidation-friendly policy for timeline freshness
Edge Security and Controls
- Enforce HTTPS/WSS and redirect HTTP to HTTPS.
- Enable WAF baseline protections for common web exploits.
- Apply rate limits for abusive request patterns (especially auth/admin paths).
- Keep origin exposure minimized (prefer Cloudflare-only public ingress where feasible).
Worker and Header Policy
- Use Worker logic for immutable cache headers and request normalization where cache rules alone are insufficient.
- Keep CORS behavior explicit for HLS resources to support browser playback.
Operations and Validation
- Validate cache behavior after deployment:
- expected cache status progression (MISS -> HIT)
- manifest freshness under active playout
- Track edge metrics and adjust:
- cache hit ratio
- request volume and error rates
- ToS/cost posture for sustained traffic profiles
Security & Moderation
Security is split between edge controls (Cloudflare), reverse proxy controls (nginx), and application controls (chat/admin backend).
Transport and Edge Protection
- Public traffic uses HTTPS/WSS at the edge.
- Cloudflare WAF and rate limiting are part of the baseline (see cache-rules).
- Cloudflare Worker can enforce immutable cache headers for segment paths (
infrastructure/cloudflare/worker.js).
Reverse Proxy Enforcement (nginx)
- Admin UI and admin API paths are protected by
auth_requestbefore reaching backend handlers (/admin/,/api/admin/). - Unauthorized admin UI requests are redirected to
/admin/login/. - Unauthorized admin API requests return JSON
401from a dedicated location handler. - WebSocket upgrade headers are explicitly forwarded for
/ws.
Admin Authentication and Session Security
- Admin authentication is cookie-session based (
linearvoid_admin_session). - Session cookie controls in backend include
HttpOnly,SameSite=Strict, andSecurewhen request is HTTPS (derived from TLS orX-Forwarded-Proto). - Session expiration is enforced server-side.
Viewer User Accounts
- Register/login/logout via
POST /api/auth/register,POST /api/auth/login,POST /api/auth/logout; current session viaGET /api/auth/session. - Passwords are hashed with bcrypt; sessions use a
linearvoid_user_sessioncookie with a 30-day default TTL (USER_SESSION_TTL_HOURS). - Viewer sign-in UI (username/password) lives in
frontend/index.html.
Rate Limiting and Abuse Controls
- Chat message rate limiting uses Redis
INCR + EXPIREwindowing. - Current baseline in chat handler:
>10messages within10striggers a user-facing throttle response. - If Redis is unavailable, presence/rate-limit features degrade gracefully (warning logged; core chat remains available).
CORS and Cross-Origin Behavior
- HLS endpoints exposed by nginx include explicit
Access-Control-Allow-Origin: *for player compatibility. - Admin/authenticated routes are expected to run same-origin behind nginx auth gates.
Moderation and Privacy
- Moderation commands (
/mod,/mute,/ban) are handled in backend chat flow and persisted through chat storage paths. - Private DMs are application-level private channels, not end-to-end encrypted in current architecture.
- Moderation and audit retention policies should be formalized in runbook/ops docs as part of production hardening.
Chat
WebSockets + Redis (presence, pub/sub, rate limits) + DB (history, audit). Public rooms + on-demand private chats. Scales independently of video.
- Private DM history (
GET /api/dm/history, paginated) and search (GET /api/dm/search) are backed by persisted DMs. - Viewer UI includes a typing indicator, user-list toggle, and DM search.
Full details, cache rule table, bitrate/storage calculations, cost model, example configs, ToS notes, and implementation phases are in adr/ADR-001.md.
Architecture Decisions
The primary decisions are captured in the following consolidated ADRs (all located in adr/):
- ADR-001: Overall architecture, HLS + Cloudflare R2 caching strategy, storage calculations, cost model, risks.
- ADR-002: Chosen project folder/module structure (frontend, backend/playout, backend/chat, infrastructure, scripts, monitoring) and rationale.
- ADR-003: Technology stack, programming language choices per module (TypeScript frontend, Go for chat/playout wrapper, Bash/Python scripts), Docker-first approach, and explicit documentation of all implicit assumptions.
- ADR-004: Video admin web interface (upload, playlist management, schedule overrides, normalization enforcement).
- ADR-005: Web admin interfaces for user management, moderation dashboard, analytics, and system configuration.
- ADR-006: Data storage strategy (SQLite vs PostgreSQL for v1, migration path, admin interface capabilities).
- ADR-007: Redis usage (exact use cases, TTL policies, persistence, fallback behavior, admin configuration).
These ADRs together form the canonical decision log.
Summary of Key Decisions (from ADR-001 through ADR-007):
- Self-managed US-East playout (Hetzner Ashburn) with FFmpeg + MediaMTX/SRS → HLS → Cloudflare R2 + edge caching.
- Module structure that directly reflects the arc42 building blocks and context diagram.
- Go preferred for performance-critical services; TypeScript for frontend; Docker for all deployment.
- Admin interfaces for video management, moderation, analytics, and system configuration using HTMX + custom CSS.
- SQLite as primary database for v1 with PostgreSQL migration path if needed.
- Redis for rate limiting, presence, pub/sub, session storage with defined TTL policies and fallback behavior.
Quality Requirements
See Quality Goals table in the Introduction section. Primary scenarios focus on cost (near-zero egress), availability (auto-restart + fallback), scalability (edge caching), and usability (synchronized player + responsive chat).
Performance Benchmarks and Capacity Planning
Baseline SLO Targets (v1)
- Playback startup: target < 4s after first manifest request on warm cache.
- Steady-state playback: no segment starvation during normal operation.
- End-to-end HLS latency: expected 15-35s (standard HLS trade-off).
- Chat delivery latency: p95 < 500ms inside same region under normal load.
- Admin page responsiveness: p95 API response < 1s for common read operations.
Workload Assumptions
- Primary audience in US; origin in US-East.
- Edge cache serves most segment traffic; origin handles manifest churn and cache misses.
- Chat traffic scales with concurrent viewers but remains low-bandwidth compared to video.
Initial Capacity Model
- Video bitrate envelopes (from ADR-001):
- 720p30: ~2.6-4.2 Mbps total with audio.
- 1080p30: ~5.1-8.2 Mbps total with audio.
- Storage planning for 48h rolling library:
- 720p30: ~57-90 GB raw (allocate ~80-110 GB with headroom).
- 1080p30: ~110-175 GB raw (allocate ~130-180 GB with headroom).
- Cost behavior:
- Viewer scale is mostly constrained by cache efficiency and request volume, not by direct origin egress.
Benchmark Plan
- Playback path:
- Measure time-to-first-frame and rebuffer events from multiple US regions.
- Track manifest freshness and segment availability at edge.
- Chat path:
- Load-test concurrent websocket connections and message fan-out.
- Measure p50/p95 send-to-receive latency and Redis fallback behavior.
- Admin/API path:
- Benchmark playlist/system endpoints for p95 latency and error rate.
- Platform path:
- Observe TSDB ingestion, query p99, and GC pause behavior in Grafana.
Capacity Triggers and Actions
- Trigger: sustained viewer growth with rising cache-miss ratio.
- Action: tighten cache policy, enable/verify Tiered Cache, review manifest TTL.
- Trigger: chat p95 latency drift or websocket error growth.
- Action: scale chat service, tune Redis, separate noisy moderation workloads.
- Trigger: Prometheus WAL/compaction failures or query saturation.
- Action: tune retention/cardinality, increase resources, split dashboards/queries.
- Trigger: origin CPU/memory pressure on playout host.
- Action: optimize FFmpeg profile, isolate services, add secondary origin.
Scaling Strategy
Scaling is designed to preserve cost efficiency first, then increase throughput and resilience in controlled phases.
Phase 1: Vertical + Cache-First (Current Baseline)
- Single primary origin in US-East with strong Cloudflare edge caching.
- Optimize segment immutability and manifest TTL before adding infrastructure.
- Scale compute vertically when CPU/memory pressure appears on playout/chat host.
Phase 2: Service Separation
- Split playout, chat, and observability workloads across dedicated nodes/VMs.
- Isolate noisy workloads (analytics, heavy admin tasks) from latency-sensitive chat paths.
- Introduce managed PostgreSQL/Redis or dedicated state nodes where operationally justified.
Phase 3: Multi-Origin Resilience
- Add warm standby or active-secondary origin.
- Use health-aware DNS/origin failover strategy.
- Keep HLS path and cache contract stable so viewer impact during failover is minimized.
Phase 4: Regional and Throughput Expansion
- Add additional origin region only when audience distribution and latency justify it.
- Keep chat affinity strategy explicit (regional routing, state replication considerations).
- Continuously evaluate cache miss economics vs additional origin complexity.
Scaling Triggers
- Rising cache miss ratio with sustained traffic growth.
- Chat p95/p99 latency drift or websocket error growth.
- Persistent origin resource saturation despite optimization.
- Increased blast-radius concerns from single-host dependency.
Disaster Recovery and Backup Procedures
Recovery Targets
- RPO target: <= 24h for chat/moderation/admin metadata.
- RTO target: <= 60 minutes for restoring baseline streaming + chat operations.
Backup Scope
- Persistent service volumes:
- Postgres data (
postgres-datavolume). - Redis append-only persistence (
redis-datavolume) where enabled. - Prometheus long-term TSDB (
prometheus-datavolume) if historical metrics retention is required. - Grafana state (
grafana-datavolume) if dashboards/users are managed in-instance.
- Postgres data (
- Host-mounted state directories:
data/chatdata/playlistdata/media(authoritative source may be R2/object storage)data/hls(optional; can be regenerated by playout)
- Configuration and infra source of truth:
- Compose files, nginx config, monitoring configs, and environment template.
Backup Cadence
- Daily logical Postgres backups (plus weekly full snapshot).
- Daily sync of chat/playlist state directories.
- Weekly integrity check by restoring backups into an isolated environment.
- Retention policy baseline:
- Daily backups: 14 days
- Weekly backups: 8 weeks
- Monthly backups: 6 months
Recovery Runbook (Minimum)
- Restore infrastructure configuration (compose, nginx, env, monitoring config).
- Restore Postgres and Redis persisted state.
- Restore
data/playlistand chat persistence directories. - Rehydrate media from authoritative object storage if local media is missing.
- Start stack and validate in order:
playouthealth endpointchathealth endpoint- nginx
/health - HLS playlist availability
- websocket chat connectivity
- Verify admin authentication and moderation audit visibility.
- Announce recovery and continue elevated monitoring for at least one playlist cycle.
Failure Modes and Strategy
- Single-host loss: redeploy stack on standby host, restore data backups, repoint DNS/edge origin.
- Storage corruption: restore last known-good snapshot and replay latest logical backups.
- Partial service failure: replace only impacted service and restore scoped data (for example Postgres only).
Document and rehearse this runbook quarterly; recovery that is not exercised is not reliable.
Risks and Technical Debts
- Cloudflare ToS risk for heavy video on free tier (use R2 path; fallback to BunnyCDN if challenged).
- HLS latency (15–35s) may affect perceived chat synchronization.
- Single VPS as SPOF (mitigate with DNS failover in Phase 3).
- Upfront effort for video normalization and cache tuning.
- Moderation/abuse prevention operational load at scale.
- R2 request costs on cache misses (optimize with Tiered Cache; measure in prod).
- Deferred items: LL-HLS, ABR, E2EE, paid access controls, full Docker Compose.
Technical Debt: Detailed service configs, CI/CD, and advanced resilience are left for implementation ADRs.
Glossary
| Term | Definition |
|---|---|
| HLS | HTTP Live Streaming – protocol using .m3u8 manifests and 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 that cache and serve content close to viewers. |
| Playout | Continuous generation of a scheduled linear program from a playlist. |
| Normalization | Pre-processing videos to consistent codec/fps/keyframe settings for seamless playback. |
| Cache Rule | Cloudflare configuration defining caching behavior, TTL, and 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 documentation, standard video bitrate calculators. All original root ADR-001-*.md files have been consolidated and can be archived/deleted.