Skip to content

feat(firehose): websocket firehose service - #200

Open
extreme4all wants to merge 10 commits into
developfrom
feat/firehose
Open

extreme4all wants to merge 10 commits into
developfrom
feat/firehose

Conversation

@extreme4all

@extreme4all extreme4all commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds a firehose service: streams kafka topics to browsers and systems over websockets.

New bricks: bases/bot_detector/firehose + projects/firehose (compose service, port 8000). It consumes event_queue and reuses the apiUser/permissions tables as a topic-scoped allowlist.

The browser frontend lives in _infra/_firehose_frontend (vite + react + tailwind) as a manual test/showcase client — not a brick, not a container; run it with npm.

Endpoints (api only, no html)

Route Purpose
GET /firehose/topics available topics (json)
WS /firehose/{topic} message stream; ?anonymous=1 forces the shared anonymous group
GET /login, /login/callback discord oauth2 authorization code flow (state cookie, CSRF check); callback sets the cookie and redirects to /me
GET /me identity + token for the current credential
GET /metrics prometheus metrics

Auth (stateless)

  • credential = discord access token (X-API-Key header for systems, cookie for browsers) — validated against discord on every connect; no local tokens or sessions
  • keyed group requires an active apiUser row with username = "discord_{id}" and the topic-scoped firehose.<topic> permission; otherwise anonymous; invalid tokens are rejected with 403
  • static dev keys (settings) keep the service usable without a database

Consumers & topics

  • shared, refcounted consumer per (topic, group) (firehose-anonymous-<topic> latest / firehose-<topic>-<user>-<hash> earliest); connections of a group are broadcast to
  • topics are a hardcoded catalog (topic -> event_queue model) — only players.scraped exposed for now
  • poison messages and transient consumer errors are skipped with backoff

Frontend (manual test client)

_infra/_firehose_frontend — topic picker, token field, anonymous/connect/disconnect, pretty-printed message log. Same three access modes as the api (anonymous, discord login, pasted token). Vite dev server proxies /firehose, /login, /me to the api (FIREHOSE_URL, default localhost:5000); no CORS needed.

Metrics

firehose_consumers, firehose_connections, firehose_messages_total, firehose_bytes_total — per topic and anonymous|keyed, on /metrics.

Related changes

  • fix(event_queue): kafka value deserializer never raises (a poison message used to kill aiokafka's fetcher and stall the consumer silently)
  • feat(database): ApiUserRepo.get_user_by_token + optional-token has_permission (permission check by user id)
  • chore(infra): firehose compose service (dev hot reload) and firehose permission seed; RESET_TOPICS=false
  • refactor(firehose): embedded html pages removed from the base; frontend extracted to _infra/_firehose_frontend; /firehose/topics is json; /login/callback redirects to /me
  • chore(infra): firehose api service in compose-dev; .dockerignore covers .git/node_modules/caches; root Dockerfile uv pinned to 0.5.13
  • chore(firehose): ci workflow (firehose.yml: push = lock/lint/format/tests, dispatch = build to quay; deploy job pending a deployment.yaml); compose firehose image is target: prod + healthcheck; requires-python >=3.12,<3.13 so the local venv matches containers; mypy configured and green for the firehose base
  • issue Dockerfiles: consolidate the 17 per-project copies into a shared template? #201: consolidate the 17 per-project Dockerfiles (open question)

Test plan

  • unit: auth repo (static keys, discord reject, allowlist/permission matrix, db-failure degradation), queue repo (groups, offsets, topics), consumer manager (refcount, per-topic streams, gauges), connection manager (group-scoped broadcast, dead-client drop)
  • e2e against the compose stack: anonymous broadcast, keyed replay via header/cookie, 403 for invalid tokens and unknown topics, /login flow with a real discord account, /metrics counters while streaming
  • frontend: npm run build green; flows verified through the vite proxy against the compose-dev api (topics json, page 200)
  • bd/firehose:prd builds; compose files validate; mypy/ruff/pytest green locally

extreme4all added 10 commits September 25, 2026 00:05
A raising value_deserializer kills aiokafka's internal fetcher task,
which silently stalls the consumer on the first poison message. Invalid
JSON now deserializes to a marker dict that fails model validation
downstream, surfacing as a normal error-as-value to callers.
- ApiUserRepo.get_user_by_token: resolve an apiUser row by its token,
  used for stateless credential validation
- has_permission: make the legacy token filter optional so permissions
  can be checked by user_id or user_name alone
The repository raises "voter_player_id or voter_player_name must be
provided"; the test still expected the old wording.
- docker-compose: firehose dev service (bind-mounted sources for hot
  reload, graceful shutdown timeout) and RESET_TOPICS=false so topic
  resets stop breaking live consumers
- mysql init: seed the firehose permission and document how ops
  allowlists a discord user (apiUser row + firehose permission)
Streams kafka topics to browsers and systems over websockets.

- GET /firehose: topic index; GET /firehose/{topic}: viewer page;
  WS /firehose/{topic}: message stream (?anonymous=1 forces the shared
  anonymous group)
- auth: discord access token (X-API-Key header or cookie) validated
  against discord; the identity must exist as
  apiUser.username = "discord_{id}" and hold the topic-specific
  firehose.<topic> permission; otherwise anonymous; static dev keys via
  settings keep the service usable without a database
- /login + /login/callback: discord oauth2 authorization code flow,
  sets the credential cookie; /me returns identity + token
- consumer_manager: shared refcounted consumer per topic+group;
  connection_manager: group-aware broadcast; poison messages and
  transient consumer errors are skipped with backoff
- metrics: firehose_consumers/connections/messages_total/bytes_total
  per topic and type, exposed on /metrics
- topics are a hardcoded catalog (topic -> event_queue model); only
  players.scraped is exposed for now
- drop viewer.py and the html topics/login-callback pages from the base
- api: json GET /firehose/topics, /login/callback redirects to /me
- new _infra/_firehose_frontend: vite + react + tailwind manual test
  client, run locally via npm (FIREHOSE_URL picks the api target)
- compose-dev: add firehose service (dev target, hot reload, 8000:5000)
- .dockerignore: exclude .git, node_modules, dist, caches, spec/test/development
- root Dockerfile: uv 0.5.4 -> 0.5.13, fix broken uvx COPY
- ci: firehose.yml (push: lock/lint/format/tests, dispatch: build to quay;
  deploy job pending a firehose deployment.yaml)
- compose: firehose service to target prod, drop bind mounts, healthcheck
- projects/firehose: requires-python >=3.12,<3.13 to match containers
- mypy: source-based resolution for bricks, scoped to the firehose base
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant