feat(cloudflare): add linearvoid-media edge worker for R2 HLS delivery

Replace the ad-hoc `worker.js` with a self-contained `linearvoid-media`
worker under `infrastructure/cloudflare/` that owns `media.linearvoid.tv`.
It serves HLS objects from the R2 bucket, enforcing immutable Cache-Control
on segments, a short TTL on live manifests, plus HLS CORS, OPTIONS
preflight, Range support, and a `/health` probe. Includes wrapper config
with production domain, Vitest unit tests, Miniflare R2 integration tests,
automated deploy workflow, and docs updates.
This commit is contained in:
zwitschi committed 2026-09-03 08:52:31 +02:00
1 parent 5f1f796a20
commit 8c5e70959b
16 files changed
+3359 -42

No files matched your search

+2 -2
View File
@@ -70,7 +70,7 @@ flowchart LR
2. S3 endpoint: `https://<account-id>.r2.cloudflarestorage.com` → `R2_ENDPOINT`.
3. Attach `media.linearvoid.tv` as custom media domain → `MEDIA_DOMAIN`; add CORS allowing `https://linearvoid.tv` GET for HLS delivery.
4. Point `linearvoid.tv` (+ `www` redirect) at the app entrypoint; keep `media.*` on R2. Proxied DNS, TLS **Full (strict)**.
5. Apply the `.m4s`/`.m3u8` cache rules from [`cache-rules.md`](./infrastructure/cloudflare/cache-rules.md); enable Smart Tiered Cache; optionally deploy `infrastructure/cloudflare/worker.js` for header normalization.
5. Apply the `.m4s`/`.m3u8` cache rules from [`cache-rules.md`](./infrastructure/cloudflare/cache-rules.md); enable Smart Tiered Cache; deploy the [`linearvoid-media` worker](../infrastructure/cloudflare/) on `media.linearvoid.tv` (replace the R2 bucket's direct custom-domain binding on that host) to enforce immutable segment caching + HLS CORS.
6. Verify: `aws s3 ls --endpoint-url $R2_ENDPOINT`; check cache hit ratio after serving traffic.
## Running the Application
@@ -129,7 +129,7 @@ Coolify replaces the Gitea CD `DOCKER_HOST` orchestration (Cloudflare DNS/R2/cac
### Production (Hetzner VPS + Cloudflare)
1. Provision CX22 in Ashburn, VA (ADR-001).
2. Cloudflare: R2 bucket (`enam` hint), `media.linearvoid.tv` custom domain + cache rules, `linearvoid.tv`/`www` at the app entrypoint with `www`→apex redirect, Full (strict) TLS, deploy `infrastructure/cloudflare/worker.js`.
2. Cloudflare: R2 bucket (`enam` hint), deploy the [`linearvoid-media` worker](../infrastructure/cloudflare/) on `media.linearvoid.tv` + cache rules, `linearvoid.tv`/`www` at the app entrypoint with `www`→apex redirect, Full (strict) TLS.
3. Deploy from the project: `docker compose -f docker-compose.prod.yml up -d` (or pull from Gitea registry); manage under systemd.
4. Monitoring: Grafana dashboards from `monitoring/grafana/`.
+15 -1
View File
@@ -4,6 +4,20 @@ Linear Void architecture is documented using the [arc42 template](https://arc42.
> 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.).
@@ -386,7 +400,7 @@ _Cache rules baseline_: `.ts`/`.m4s` Cache Everything, long TTL, immutable; `.m3
_Edge security/controls_: enforce HTTPS/WSS + redirect HTTP; WAF baseline; rate limits on abusive patterns (auth/admin); minimize origin exposure.
_Worker & header policy_: Worker logic for immutable cache headers/request normalization where cache rules insufficient (`infrastructure/cloudflare/worker.js`); explicit CORS for HLS.
_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.
+19 -16
View File
@@ -2,32 +2,35 @@
Linear Void uses Cloudflare Workers for two purposes:
| Product | Purpose | What |
| ----------- | ------------------ | ------------------------------------------------------------------------------------ |
| **Workers** | Static SPA hosting | Vite-built frontend (`frontend/dist`) served as Worker static assets |
| **Workers** | Edge cache logic | `infrastructure/cloudflare/worker.js` — adds immutable cache headers to HLS segments |
| Product | Purpose | What |
| ----------- | ------------------ | -------------------------------------------------------------------------------------------- |
| **Workers** | Static SPA hosting | `linearvoid-frontend` (`frontend/wrangler.toml`) — Vite SPA (`frontend/dist`) static assets |
| **Workers** | Edge HLS delivery | `linearvoid-media` (`infrastructure/cloudflare/`) — R2-bound Worker on `media.linearvoid.tv` |
Both are managed under **Workers & Pages** in the Cloudflare dashboard.
## Dashboard URLs
| Resource | URL |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------- |
| Workers & Pages overview | `https://dash.cloudflare.com/deb11491c54ec53a24772e98ac6d8128/workers-and-pages` |
| Worker: linearvoid | `https://dash.cloudflare.com/deb11491c54ec53a24772e98ac6d8128/workers/services/view/linearvoid/production/settings` |
| API tokens | <https://dash.cloudflare.com/profile/api-tokens> |
| Resource | URL |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
| Workers & Pages overview | `https://dash.cloudflare.com/deb11491c54ec53a24772e98ac6d8128/workers-and-pages` |
| Worker: linearvoid-media | `https://dash.cloudflare.com/deb11491c54ec53a24772e98ac6d8128/workers/services/view/linearvoid-media/production/settings` |
| API tokens | <https://dash.cloudflare.com/profile/api-tokens> |
## Worker: `linearvoid` (edge cache headers)
## Worker: `linearvoid-media` (edge HLS delivery)
The Worker at `infrastructure/cloudflare/worker.js` runs on the `linearvoid` Workers service. It intercepts requests and adds immutable `Cache-Control` headers to `.m4s` segment files.
The Worker in [`infrastructure/cloudflare/`](../../infrastructure/cloudflare/) runs on the `linearvoid-media` Workers service. It owns the `media.linearvoid.tv` host, serves HLS objects from the R2 bucket (bound as `MEDIA_BUCKET`), and stamps immutable `Cache-Control` on segments plus a short TTL on live manifests (CORS + Range supported). See its [README](../../infrastructure/cloudflare/README.md).
Deploy via Wrangler CLI:
Test / deploy:
```bash
npx wrangler deploy infrastructure/cloudflare/worker.js
cd infrastructure/cloudflare
npm install && npm test # unit + Miniflare R2 tests
npx wrangler deploy # preview/test target
npx wrangler deploy --env production # attach media.linearvoid.tv
```
Or paste into the Workers dashboard at the URL above (Quick Edit).
Attach the production custom domain only after removing `media.linearvoid.tv` from the R2 bucket's direct custom-domain binding. Automated production deploy: [`.gitea/workflows/workers-media-deploy.yml`](../../.gitea/workflows/workers-media-deploy.yml).
## Worker: static frontend
@@ -56,9 +59,9 @@ Backend services (playout, chat, nginx) read their own runtime variables (`ADMIN
## Custom domains
- `linearvoid.tv` → Worker (static SPA)
- `linearvoid.tv` → Worker (static SPA, `linearvoid-frontend`)
- `www.linearvoid.tv` → redirect to `linearvoid.tv`
- `media.linearvoid.tv` → R2 bucket (separate; not a Worker)
- `media.linearvoid.tv` → Worker (`linearvoid-media`, R2-bound) serving the `linearvoid-media` bucket
## Related
+1 -1
View File
@@ -16,7 +16,7 @@ Deploy the Linear Void frontend (Vite + TypeScript SPA) from Gitea to Cloudflare
3. **Account ID** from Cloudflare dashboard overview → store as `CLOUDFLARE_ACCOUNT_ID`
4. **Wrangler config** (`frontend/wrangler.toml`) binding the `frontend/dist` output as static assets.
> **Naming conflict:** Workers share a namespace under "Workers & Pages". The edge-cache Worker (`infrastructure/cloudflare/worker.js`) already uses the `linearvoid` name, so the SPA Worker is named `linearvoid-frontend` (see `frontend/wrangler.toml`).
> **Naming:** Workers share a namespace under "Workers & Pages". The media edge Worker is `linearvoid-media` ([`infrastructure/cloudflare/`](../../infrastructure/cloudflare/wrangler.toml), deployed via `.gitea/workflows/workers-media-deploy.yml`) and the SPA Worker is `linearvoid-frontend` (`frontend/wrangler.toml`, deployed via `.gitea/workflows/workers-deploy.yml`).
## Project-specific values