Files
HIPCTF2/docs/guides/scoreboard-stream.md
T

3.7 KiB

type, title, description, tags, timestamp
type title description tags timestamp
guide Scoreboard Stream How fresh solves are broadcast to clients — both the public unauthenticated scoreboard SSE stream and the authenticated combined /api/v1/events stream that the /scoreboard page consumes.
guide
scoreboard
sse
stream
public
authenticated
2026-07-23T05:10:00Z

Overview

There are two scoreboard-related SSE streams in the codebase:

  1. The public stream GET /api/v1/scoreboard/stream, owned by SystemController (backend/src/modules/system/system.controller.ts), is the @Public() @Sse('scoreboard/stream') handler that pushes a flattened solve payload every time the scoreboard hub publishes a new solve. It is meant for unauthenticated spectators (e.g. landing-page leaderboard widgets).
  2. The authenticated combined stream GET /api/v1/events (backend/src/modules/challenges/events.controller.ts) merges status ticks, general settings pushes, and solve frames into a single JWT-gated transport. The Scoreboard Page subscribes here and reacts to the solve event type to mutate all four tabs in real time.

Public endpoint — GET /api/v1/scoreboard/stream

The handler (backend/src/modules/system/system.controller.ts) opens an SSE stream that pushes a flattened solve payload every time a new solve is published to the scoreboard hub.

Push path (both streams)

  1. The challenge solve path (admin validation, player submit via ChallengesService.submitFlag) inserts a new solve row.
  2. The same code path calls SseHubService.publish('scoreboard', payload) (backend/src/common/services/sse-hub.service.ts).
  3. Subscribers to either stream receive the payload:
    • /api/v1/scoreboard/stream emits a flattened shape (challengeId, userId, pointsAwarded, rankBonus, solvedAt) for anonymous spectators.
    • /api/v1/events emits a richer SolveEventPayload (with both snake_case and camelCase aliases, plus position, live_points, solve_count, etc.) for the authenticated SPA.

The hub is in-process; multi-replica deployments need a shared pub/sub to fan out across pods (not in scope for this repo).

Authenticated consumer — /scoreboard page

The ScoreboardPage smart component subscribes to /api/v1/events (not the public stream) on ngOnInit:

  1. store.loadAll() fetches the four projections from /api/v1/scoreboard/{ranking,matrix,event-log,graph} in parallel and populates the initial state.
  2. store.wireSse(() => authenticatedEventSource.open('/api/v1/events')) opens the authenticated SSE transport via AuthenticatedEventSourceService (which adds the Bearer token and the 'unauthorized' 401/403 channel).
  3. The store listens for the solve event type and mutates all four tabs (Ranking, Matrix, Event Log, Score Graph) using pure helpers in scoreboard.pure.ts (parseSolveEventIntoRanking, mutateMatrixFromSolve, applySolveToGraph, dedupEventLogBySolveId).
  4. On transport error the store schedules a reconnect with an exponential backoff (1s → 30s cap); the solve event type resumes mutation as soon as the stream is back.
  5. On ngOnDestroy, store.stop() aborts the transport and clears the reconnect timer.

See Scoreboard Page for the full tester-facing contract (tabs, test IDs, empty states, error states) and Challenges Endpoints for the request and response shapes.

See also