Skip to content

Architecture

Iain Smith edited this page Aug 31, 2026 · 1 revision

Architecture

For contributors. Conventions and the full list of invariants live in CLAUDE.md; the environment reference is docs/deploy.md.

Layout

backend/    FastAPI · SQLAlchemy 2.0 async · Alembic · arq worker
frontend/   React 18 · Vite · TypeScript · TanStack Query · react-virtual · PWA
connector/  standalone agent for HDHomeRun tuners the server can't reach
docker/     entrypoint.sh — bootstraps secrets, runs migrations, dispatches web|worker
Dockerfile  multi-stage: node builds the SPA → python image (API + worker + SPA)

One image runs three roles: entrypoint web (uvicorn, after alembic upgrade head), entrypoint worker (arq app.worker.WorkerSettings), and the built SPA served by the API at /. Postgres is the only stateful store; Redis backs the arq queue and rate limiting.

Same-origin

The API serves the SPA from / (mounted after the /api router), so there is no CORS in production and the refresh cookie is first-party. Dev uses a Vite proxy to localhost:8000.

Notable invariants

  • Guide timezone — every surface (grid, agenda, cards, sheet, exports) renders times in channel.timezone = per-source override → tenant default → UTC, plus a per-channel clock shift. Never viewer-local.
  • Channel refresh — sources reconcile channels in place by a stable dedupe key, so unchanged channels keep their id and their programmes.
  • EPG fan-out — one XMLTV key can map to several channel ids (East/West feeds sharing a tvg-id); a programme row is written per channel.
  • Watchlist items are snapshots — a programme-kind item stores channel + start + title, not an FK to programme (an EPG refresh recreates those rows). A watchlist_notification ledger makes reminders send-once.
  • Export tokenTenant.export_token_hash is the sha256 of a shown-once token; it gates the unauthenticated /api/exports/* routes via ?token=.
  • Email is fail-open — a provider error is logged, never raised.
  • SourceKind is native_enum=False (VARCHAR) — new kinds need no migration.

Workflow

Feature branch → PR → squash-merge with --delete-branch. CI runs the backend / connector / frontend / image jobs. Releases are cut by pushing a tag: v* (app image) or connector-v* (connector image + wheel).

Clone this wiki locally