Skip to content

Architecture

Three deployable pieces.

server (Go)

Single binary. HTTP API, recorder, EventSub manager, scheduler, SSE bus.

dashboard (React)

Single-page app. Vite build; the recorder serves the bundle in production.

relay (Cloudflare Worker)

Optional public webhook ingress. Stateless fan-out backed by a Durable Object buffer.

The server is one Go process. Postgres and SQLite are both supported behind one repository interface. ffmpeg and ffprobe are required at runtime.

  • Directoryserver/
    • Directorycmd/server/ entrypoint
    • Directoryinternal/
      • Directoryapi/ HTTP routes (Chi) + tRPC mount
      • Directoryserver/ HTTP lifecycle and router wiring
      • Directoryauth/ Twitch OAuth handler
      • Directorysession/ encrypted token store, cookie sessions
      • Directoryconfig/ TOML + env loader
      • Directorydatabase/ pgx pool / sqlite open + migrate runner
      • Directoryrepository/
        • Directorypgadapter/ Postgres adapter (sqlc-generated)
        • Directorysqliteadapter/ SQLite adapter (sqlc-generated)
      • Directorytwitch/ Helix client + generated EventSub types
      • Directoryservice/
        • Directoryeventsub/ subscriptions, quotas, reconcile
        • Directorystreammeta/ title/category hydration + watcher
        • Directorycategoryart/ box-art backfill
        • Directoryschedule/ scheduled-recording rules
      • Directorydownloader/
        • Directoryhls/ segment fetcher
        • Directoryremux/ ffmpeg wrapper
        • Directoryprobe/ ffprobe wrapper
        • Directorythumbnail/ JPEG generator
      • Directoryscheduler/ task runner with DB-persisted state
      • Directoryeventbus/ generic Topic[T] pub/sub for SSE
      • Directoryrelayclient/ WebSocket agent for Connect
      • Directorystorage/ local + S3 backend
      • Directorylogger/ slog setup
    • Directorymigrations/ versioned .up/.down SQL pairs
    • Directoryqueries/ sqlc query files
    • Directorytools/twitch-api-gen/ generator for Helix + EventSub types
    • config.toml operational config
    • .env.example credentials and paths

cmd/server/main.go wires everything in this order:

  1. Load config.toml and .env. Set up the slog logger.
  2. Open the database (pgx pool or SQLite via database/sql) and run embedded migrations.
  3. Build the matching Repository adapter.
  4. Construct the Twitch Helix client, encrypted session manager, and storage backend.
  5. Construct the downloader and call Resume() to pick up jobs left in RUNNING by a previous process.
  6. Open the SSE event bus and start the HTTP server in a goroutine.
  7. If SERVER_MODE=relay, dial the Connect relay and start the replay agent.
  8. Reconcile EventSub subscriptions against the current channel set, or start the Helix live poller when SERVER_MODE=poll.
  9. Register tasks and start the scheduler.
  10. Block on SIGINT/SIGTERM, then stop the scheduler, HTTP server, and logger in reverse order.

Postgres and SQLite share one Repository interface in internal/repository/. sqlc turns the SQL files in queries/{postgres,sqlite}/ into typed Go in pgadapter/pggen/ and sqliteadapter/sqlitegen/.

Migrations live in migrations/{postgres,sqlite}/ as numbered NNN_name.up.sql / NNN_name.down.sql pairs and are embedded into the binary. They run automatically on start; the schema_migrations table tracks applied versions.

ComponentWhereWhat it does
Downloaderinternal/downloader/HLS pull → segment fetch with retry → ffmpeg remux → ffprobe → thumbnail → upload. State in DB so crashes resume cleanly.
EventSub managerinternal/service/eventsub/Boot reconcile (delete orphans, create missing) and per-call quota snapshots.
Schedulerinternal/scheduler/One ticker, 15 s cadence. Each task has its own row with next_run_at, is_enabled, last_error.
Standard tasksinternal/scheduler/tasks.goEventSub reconcile, quota snapshot, category-art sync, retention sweeps for fetch/event logs and webhook payloads, session and app-token cleanup.

React 19 SPA, Vite-built. TanStack Router for file-based routes, TanStack Query plus a tRPC v11 client (HTTP batching + SSE on the same client). UI is Base UI v1 scaffolded with shadcn v4; styling is Tailwind v4. i18n is i18next with English and French; a parity test prevents missing keys.

In dev, Vite proxies /api/* and /trpc/* to the recorder on port 8080. In production, the recorder serves the prebuilt dist/ directory directly so the SPA and API share an origin.

Types and Zod schemas come from the Go side via task trpcgen and are committed under dashboard/src/api/generated/.

Cloudflare Worker plus a Durable Object. Two endpoints:

  • POST /u/{token} — accepts arbitrary bytes, fans out to subscribers, replies 202 (or echoes a synchronous response for EventSub challenges).
  • GET /u/{token}/subscribe — WebSocket. Replays the 5-minute buffer, then streams new events.

Stateless except for the Durable Object’s per-token buffer. No Twitch credentials, no payload inspection. See Relay protocol for the wire format.

IntegrationWhereNotes
Twitch Helixinternal/twitch/client.goOAuth, streams, games, users, follows, GQL playback tokens.
Twitch EventSubinternal/service/eventsub/REST CRUD over generated bindings.
Generated typestools/twitch-api-gen/Parses Twitch HTML docs. Snapshots committed; CI checks drift.
Connect relayinternal/relayclient/Outbound WebSocket; replays signed EventSub frames to a local handler.
tRPCbefabri/trpcgoServer side. task trpcgen emits the dashboard’s TypeScript client + Zod schemas.
ffmpeg / ffprobeinternal/downloader/{remux,probe}/Required at runtime.