Architecture
How HowlCast fits together — workers, data flow, storage layout.
For the canonical architecture doc see docs/architecture.md in the repo. This page is the public-friendly summary.
Big picture
┌─────────────────────┐
│ Browser │
│ (Next.js + React) │
└──┬──────────────┬───┘
│ │
same-origin │ │ subdomain
/ │ │ api.tv.your-domain.tv
▼ ▼
┌──────────────┐ ┌──────────────┐
│ WEB Worker │ │ API Worker │
│ tv │ │ tv-api │
│ Next.js │ │ Hono + tRPC │
└──────┬───────┘ └──────┬───────┘
│ │
└────────┬─────────┘
▼
┌──────────┐
│ D1 │
│ R2 │
│ KV │
└──────────┘GetStream sits outside this — RTMPS in from OBS, HLS / chat out to the browser, webhooks back into the api worker.
Two workers, one origin
The tv worker (Next.js via OpenNext) serves the channel page, dashboard, setup wizard, and an /api/* rewrite that proxies tRPC + auth to the api worker via same-origin. Cookies stay first-party.
The tv-api worker (Hono + tRPC) owns auth, tRPC routers, GetStream webhooks, branding logo upload, and crons.
See Why split workers, not one for the reasoning.
Data flow
Read (channel page)
- Browser →
GET /on web worker → SSR shell + lazy-loaded chat/player chunks - Browser →
GET /api/trpc/channel.getInfo(rewrite) → api worker - API worker → reads
channel_config,profiles,panelsfrom D1 - Browser →
GET /api/trpc/stream.isLivepolled every 10s - If live → browser → GetStream Video SDK → HLS playlist
- Always → browser → GetStream Chat SDK → channel messages
Write (go live)
- Dashboard →
POST /api/trpc/stream.provision(mutation) - API worker → POST to GetStream
videocalls - API worker → persist
streamCallId+chatChannelCid+rtmpsUrlonchannel_config - Broadcaster → OBS push → GetStream RTMPS ingress
- Dashboard →
POST /api/trpc/stream.goLive - API worker → tells GetStream to flip call public
- GetStream → POST to
/api/webhooks/getstreamwithcall.live_started - API worker → flip
liveStartedAt, openstream_sessionsrow, fire Discord - Channel page (polling) sees
isLive: true, mounts player
Storage layout
| Resource | Name | Purpose |
|---|---|---|
| D1 | howlcast-db | Auth + channel state + sessions + analytics |
| R2 | howlcast-public | Branding logos + broadcaster avatar (one-time pull) |
| R2 | howlcast-isr | OpenNext incremental-static-regen cache |
| KV | EMOTES_KV | Emote map + analytics scratch + throttle keys |
Cookies
Same-origin via the Next.js /api/* rewrite. In dev (web on :3001, api on :3000), the rewrite makes cookies first-party. In prod with custom domains, crossSubDomainCookies is enabled in Better Auth so a session cookie set on tv.your-domain.tv is sent to api.tv.your-domain.tv.
*.workers.dev host pairs don't support cross-subdomain cookies (Public Suffix List). Either use custom domains or stick to the same-origin proxy.
Crons
Two scheduled handlers on tv-api:
0 */12 * * *— emote pipeline refresh (Twitch + 7TV + BTTV + FFZ → KV)* * * * *— analytics sampler (baseline viewer snapshot during live + 7-day prune)
Why D1 + R2 + KV instead of Postgres + S3 + Redis
- D1 — free, fast for reads, lives next to the worker, schema is simple
- R2 — no egress fees, fits the budget, S3-compatible API
- KV — eventually-consistent but fine for our use cases (emote refresh, throttle, analytics scratch)
See DESIGN-DECISIONS.md.
Next: Schema.