--- type: guide title: Scoreboard Stream description: 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. tags: [guide, scoreboard, sse, stream, public, authenticated] timestamp: 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](/guides/scoreboard-page.md) 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](/guides/scoreboard-page.md) for the full tester-facing contract (tabs, test IDs, empty states, error states) and [Challenges Endpoints](/api/challenges.md) for the request and response shapes. # See also - [Scoreboard Page](/guides/scoreboard-page.md) - [Event Window](/guides/event-window.md) - [System Endpoints](/api/system.md) - [Challenges Endpoints](/api/challenges.md)