> ## Documentation Index
> Fetch the complete documentation index at: https://sportzdocs.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Knowledge Checks

> Quizzes by difficulty: test your understanding of the Sportz system.

Self-check questions grouped by level. Answers are in the toggles, so try before peeking.

## Junior

<AccordionGroup>
  <Accordion title="What protocol delivers live commentary to the browser, and why not regular HTTP requests?">
    WebSockets. Regular HTTP is request→response→done: the server can't push. WebSockets stay open so the server pushes events the instant they happen.
  </Accordion>

  <Accordion title="Where does the 'status' (scheduled/live/finished) of a match come from?">
    It's derived from `startTime` and `endTime` relative to now, not stored as a user input. Before start = scheduled, between = live, after end = finished.
  </Accordion>

  <Accordion title="What does HOST=0.0.0.0 mean and why does the container need it?">
    "Listen on all network interfaces." Inside a container, `127.0.0.1` means only the container itself, so Render's proxy couldn't reach it. `0.0.0.0` lets external traffic in.
  </Accordion>
</AccordionGroup>

## Mid

<AccordionGroup>
  <Accordion title="A new commentary event arrives over WebSocket. Trace how it reaches the screen.">
    `useWebSocket`'s `onmessage` → routes to `onCommentary` → `addEvent` → `setQueryData` prepends it to `['commentary', matchId]` → the panel re-renders → the new `CommentaryEvent` animates in.
  </Accordion>

  <Accordion title="Why is Arcjet mocked in tests instead of using a real key?">
    `protect()` makes a real network call, which would make tests flaky (network/uptime), slow (round-trip per request), and consume quota. The mock always allows, so tests are fast, offline, deterministic.
  </Accordion>

  <Accordion title="Why does the commentary query use enabled: matchId !== null?">
    So it doesn't fire when no match is selected (which would request `/matches/null/commentary` and 404). It activates automatically the moment a match is chosen.
  </Accordion>
</AccordionGroup>

## Senior

<AccordionGroup>
  <Accordion title="Why is broadcastCommentary room-scoped but broadcastMatchCreated global?">
    A new match should appear in everyone's grid (global). Commentary only matters to people watching that specific match (room-scoped, via the `matchSubscribers` map). Sending all commentary to all clients would be wasteful and leak unrelated matches' data.
  </Accordion>

  <Accordion title="Explain the three SSL cases in db.ts, and why disabling cert checks is not the same as disabling SSL.">
    Cloud (valid certs → verify), Local (self-signed → attempt SSL, skip verification), plain Postgres (no SSL → don't attempt at all). `{ rejectUnauthorized: false }` still *attempts* a handshake; `ssl: false` skips it entirely. The plain test DB refuses any handshake, so it needs `ssl: false`.
  </Accordion>

  <Accordion title="What's the single change that lets the WebSocket layer scale past one instance?">
    A pub/sub backbone (e.g. Redis): each instance publishes broadcasts to a channel and relays to its own local subscribers, because the in-process `matchSubscribers` map only knows about one instance's connections.
  </Accordion>
</AccordionGroup>

## Staff

<AccordionGroup>
  <Accordion title="Argue for keeping the synchronous in-process broadcast, then state the exact condition that invalidates the argument.">
    It's the simplest correct design for a single instance: no network, no extra infra, fully understood. It's invalidated the moment you run more than one API instance, because a broadcast on instance A never reaches a client connected to instance B. That condition, horizontal scaling, is the trigger to introduce pub/sub, and not before.
  </Accordion>

  <Accordion title="A known 500-vs-404 bug exists with a test pinning the wrong behavior. Why might that be the correct staff decision?">
    Because the fix has a non-trivial tradeoff (extra query vs coupling to PG error codes), pinning current behavior with an explanatory test makes any future fix deliberate and reviewed rather than silent, preventing a rushed choice from baking in the wrong tradeoff. A documented, test-guarded known-gap beats a hasty fix.
  </Accordion>
</AccordionGroup>

***

# By domain

The level-based sets above span topics. These sets drill a single domain deep.

## Frontend

<AccordionGroup>
  <Accordion title="Why does MatchCard use React.memo with a custom comparator rather than a default memo?">
    A default memo does a shallow prop compare, but match objects are recreated on every parent render (new reference), so shallow compare would still re-render every card. The custom comparator compares the specific fields that affect output (id, scores, status, isActive, index) by value, so a card re-renders only when its own data actually changed.
  </Accordion>

  <Accordion title="Why is the continuous 'live' pulse done in CSS but the new-event entrance in Framer Motion?">
    The pulse runs the entire session: a JS animation would hold a requestAnimationFrame loop the whole time, while CSS runs on the compositor for free. The event entrance is a one-shot, event-driven animation where Framer Motion's AnimatePresence enter/exit ergonomics are worth it. Right tool per animation type.
  </Accordion>

  <Accordion title="Why are the observability providers nested in a specific order in layout.tsx?">
    Providers that others depend on go outermost. Theme wraps everything (any provider/component may read it); Query wraps the app (components call useQuery); PostHog needs a Suspense boundary and sits inside Query; New Relic is a pure side-effect with no children dependencies, so innermost. Order encodes the dependency graph.
  </Accordion>

  <Accordion title="What breaks if you animate a card's height instead of using transform?">
    Animating height triggers layout recalculation and paint on every frame: janky, especially during high-frequency WS updates. transform/opacity are GPU-composited and skip layout/paint. The rule: never animate layout-affecting properties.
  </Accordion>

  <Accordion title="Why is the 'Live' label's color a theme-aware --live token rather than a fixed red?">
    Contrast is a relationship between text and its background, and the card background flips white ↔ near-black with the theme. One fixed red can't pass 4.5:1 on both: dark enough for white is too dark for near-black. So `--live` holds a different value per theme (#c81e1e light, #f87171 dark). Only colors on a fixed background (the always-yellow header) can be hardcoded.
  </Accordion>

  <Accordion title="The lint rule flags `setState` in an effect. Why, and what do you do instead?">
    Setting state synchronously in an effect forces a second render (render → effect → setState → render) and usually means you're storing something derivable. Prefer deriving during render (the disconnect modal became `wsStatus === 'disconnected' && !errorDismissed`), or move the update into the callback of the real external event (reconnect side effects fire from the socket's onopen, not a status-watching effect). Effects are for syncing with external systems, not for reacting to state to set more state.
  </Accordion>

  <Accordion title="Why can't you read a ref's .current during render?">
    Render must be a pure function of props + state. A ref is mutable and changing it doesn't trigger a re-render, so reading .current in render can produce output that's stale or inconsistent with what React thinks it rendered. If a value affects rendering, it must be state or props; refs are for things render doesn't need (DOM nodes, timers, previous values) and are touched only in effects/handlers.
  </Accordion>

  <Accordion title="A deployed Next.js site is fetching from localhost. What happened?">
    NEXT\_PUBLIC\_\* values are inlined into the JS bundle at build time, not read at runtime. The build ran without NEXT\_PUBLIC\_API\_URL set, so the code's `?? 'http://localhost:8000'` fallback got frozen in. Fix: set the var in the deploy env and REBUILD (a Vercel redeploy / a Docker build with the right --build-arg); changing it without rebuilding does nothing.
  </Accordion>

  <Accordion title="Why do the WS handlers dedup by id before writing to the React Query cache?">
    A live event can also be in the initial REST batch (you click Watch Live, the fetch returns the latest 50, and a WS event for that match arrives that was already in that batch) or be delivered twice. Prepending unconditionally would show it twice AND collide on the React key (the list keys by id). So addEvent/addMatch skip if the id already exists.
  </Accordion>

  <Accordion title="Matches kept stacking on screen until a refresh fixed it. What was wrong?">
    The client keeps its match cache fresh incrementally with setQueryData, but only the ADDS (match\_created) were handled. The backend also PRUNES matches, and those removals were never reflected on the client, so its copy grew forever and only re-synced on a refetch (refresh). It's a cache-consistency bug: an incrementally-updated cache must handle removals too. Fix: bound the cache write (keep all live + the watched match + newest finished up to a cap), mirroring the backend's pruning, or invalidateQueries and refetch (trading "instant" for "always consistent").
  </Accordion>
</AccordionGroup>

## Backend & real-time

<AccordionGroup>
  <Accordion title="Why must close() clear the heartbeat interval, and what went wrong before it existed?">
    setInterval keeps the Node process alive. Without close() clearing it, the test process never exited cleanly: the WS test suite hung. close() also becomes the building block for graceful shutdown on SIGTERM. (Discovered writing the WS tests.)
  </Accordion>

  <Accordion title="A malformed JSON frame arrives on the socket. What happens, and why not crash?">
    handleMessage wraps JSON.parse in try/catch and replies `{ type: 'error', message: 'Invalid JSON' }` rather than throwing. A single client's bad frame must never crash the server or affect other clients, hence defensive parsing at the boundary.
  </Accordion>

  <Accordion title="Why does Arcjet run on the WebSocket upgrade, not just HTTP routes?">
    The upgrade handshake is an HTTP request before the socket opens, and an unprotected upgrade would let bots open unlimited connections, exhausting memory (each holds a subscription Set). Arcjet on the upgrade applies a stricter sliding window (5 connections/2s) at exactly that chokepoint.
  </Accordion>

  <Accordion title="commentary is room-scoped but score_update is broadcast to ALL clients. Why the difference?">
    Commentary only matters to people watching that match (a room). But the score is shown on the match card in EVERY client's grid, so a score change must reach everyone, or other viewers' cards go stale. Room-scoping the score would be a bug; broadcasting all commentary to everyone would waste bandwidth and leak unrelated matches. The scope follows "who needs to see it." (See ADR-010.)
  </Accordion>

  <Accordion title="What does DEMO_MODE do, and why is it in-process instead of an external script hitting the API?">
    When DEMO\_MODE=true the server runs a simulator that keeps live matches and emits commentary/score updates on an interval, so the deployed app is always live for visitors with no external producer. It's in-process, writing to the DB via Drizzle and calling the broadcast functions directly, so there's no HTTP hop and thus no Arcjet to fight (a trusted producer shouldn't face public bot-detection). The honest caveat: co-locating the producer is a demo shortcut; a real pipeline would be a decoupled service/queue. (See ADR-011.)
  </Accordion>

  <Accordion title="Why does the New Relic agent need its own bootstrap.ts entry file instead of just importing it at the top of index.ts?">
    New Relic patches modules to instrument them, so it must load before those modules do. Under CommonJS, `require('newrelic')` as the first line works because requires run in the order written. Under ESM, that guarantee breaks: static imports in a file are evaluated before that file's own body runs, regardless of where they appear in the source, so even a conditional import written above `import express` would lose, because express's module gets evaluated first anyway. bootstrap.ts has no other imports of its own, so its conditional New Relic import is guaranteed to run before it dynamically imports index.ts (and everything index.ts pulls in). (See ADR-013.)
  </Accordion>
</AccordionGroup>

## Data & persistence

<AccordionGroup>
  <Accordion title="Why RESTART IDENTITY in the test reset, beyond just emptying tables?">
    It resets the serial primary-key sequence to 1. Without it, IDs climb across tests, making assertions like expect(data.id).toBe(1) depend on test execution order, which is fragile. RESTART IDENTITY makes each test's IDs deterministic.
  </Accordion>

  <Accordion title="Posting commentary for a non-existent match: what does the DB do, and what does the user see?">
    The FK constraint (commentary.matchId → matches.id) rejects the insert with Postgres error 23503. The route's generic catch turns it into a 500 'Failed to create commentary', the known 404-vs-500 gap. The DB enforces integrity correctly; the API just reports it imprecisely.
  </Accordion>
</AccordionGroup>

## DevOps, CI/CD & deploy

<AccordionGroup>
  <Accordion title="Why does the Dockerfile copy package*.json before the source code?">
    Layer caching. npm ci only re-runs when package\*.json changes; if source were copied first, every code change would invalidate the dependency-install layer and reinstall everything. Copy manifests → install → copy source orders layers from least to most frequently changing.
  </Accordion>

  <Accordion title="Render doesn't run migrations. What's the consequence and the fix?">
    On first deploy the Neon Cloud DB has no schema, so every query fails with 'relation does not exist'. Fix: run npm run db:migrate against the prod DATABASE\_URL once before/after deploy (or as a release step). Migrations are a separate concern from running the app.
  </Accordion>

  <Accordion title="Why prefer logging to stdout in production over file transports?">
    Containers are ephemeral: file logs vanish on restart and (as ISSUE-002 showed) can crash startup on permission errors. stdout is captured by the platform (Render) and is the only durable log surface in a container. Sportz logs to console-only in production, files only in dev.
  </Accordion>

  <Accordion title="CI runs on every push. Does that stop bad code reaching production?">
    Not by itself. Running CI ≠ enforcing it. With Vercel's Git-integration CD, the platform deploys whatever lands on main regardless of your GitHub Actions result. The gate is branch protection: require the CI checks + a PR on main (admin bypass off). Since production deploys from main, gating main gates production. PR previews stay ungated on purpose, since you want to preview WIP.
  </Accordion>

  <Accordion title="Why isn't the deployed smoke test part of CI?">
    It hits the live prod stack (cold-starting free-tier backend, slow, non-deterministic), so running it on every push would be flaky and pointless. It's a post-deploy check, run manually via `npm run test:e2e:deployed` (excluded from the normal suite with --grep-invert @deployed). Automating it AFTER a deploy, not as a per-push gate, is the right home for it.
  </Accordion>
</AccordionGroup>

## Testing & quality

<AccordionGroup>
  <Accordion title="The full e2e suite fails with timeouts, but every spec passes when you run it alone, and which specs fail changes between runs. What's the diagnosis?">
    An environment/concurrency problem, not broken test logic. The tell is the triad: timeouts + passes-individually + shifting failures. Here it was `next dev` compiling routes on first request: under fullyParallel, several cold compiles serialize past the 30s timeout. Running serially removes the contention (proves it), and the real fix is testing a production build.
  </Accordion>

  <Accordion title="Why do the Playwright tests run against `next build && next start` instead of `next dev`, and on port 3100?">
    A prod build serves pre-compiled routes instantly, so parallel requests don't queue (no flaky timeouts), and it's the exact build CI/Vercel ship, so you test what users run, not dev-only behavior. The dedicated port 3100 (with reuseExistingServer:false) keeps the test server off the dev server's 3000, so they coexist and the suite never silently reuses a flaky dev server.
  </Accordion>

  <Accordion title="Why mock the WebSocket with page.routeWebSocket instead of letting the test hit the real backend?">
    Determinism. A real socket to the cold free-tier backend makes the test depend on connection timing (e.g. whether the 'Connected' badge appears in time), so the same test passes or fails on network luck. Mocking at the network boundary makes the live path fully controlled, fast, and offline.
  </Accordion>

  <Accordion title="Why does the a11y scan wait for opacity:1 before running axe?">
    Mid-animation, the element's color is blended with the background, so axe computes a contrast ratio that isn't the real settled one and reports false positives. Scanning only after the entry animation finishes leaves only genuine violations.
  </Accordion>

  <Accordion title="The onboarding tour auto-opens on a first visit. Why would that break the e2e suite, and how is it prevented?">
    Playwright gives each spec a fresh context (empty localStorage), so to the tour every test looks like a first visit → it auto-opens a full-screen overlay that covers the page, breaking the a11y scan and blocking Watch-Live clicks. Prevented two ways: the app skips auto-open under automation (`navigator.webdriver`, which also protects the deployed smoke test), and the mock setup deterministically pre-sets the "seen" flag with `page.addInitScript`. General rule: any first-run popup must be suppressible in automated runs.
  </Accordion>
</AccordionGroup>

## Security & scaling

<AccordionGroup>
  <Accordion title="Does Sportz need CSRF protection today? Will it ever?">
    Not today: CSRF exploits ambient credentials (cookies) on state-changing requests, and Sportz has no cookie-based auth. The moment cookie sessions are added, CSRF becomes relevant and needs tokens/SameSite. It's a 'when auth arrives' concern, correctly N/A now.
  </Accordion>

  <Accordion title="At what user count does client-side React Query caching stop helping with DB load?">
    It never helped cross-user load: it caches per-user repeat views. 10k users each cold-loading the same match list still hit the DB 10k times. Server-side caching (Redis) addresses that, and only earns its place once measurements show the DB is the bottleneck.
  </Accordion>

  <Accordion title="Why does cursor pagination beat offset for a real-time match list?">
    Matches are fetched as a top-100 window and paginated client-side; scaling past 100 needs server-side pagination. In a real-time feed new rows are constantly inserted at the top, so OFFSET pagination drifts: `offset=100` points at different rows between requests, causing duplicates/skips. CURSOR (keyset) pagination anchors on a fixed value (`?after=<createdAt|id>` → `WHERE createdAt < :cursor`), so pages stay stable as new rows arrive. (Cursor pagination isn't built; the route accepts only `limit`.)
  </Accordion>
</AccordionGroup>

## Practical exercise

<Note>
  Stand up the full stack locally: start the test Postgres, run migrations, run all 95 backend tests, then `npm run dev:docker` and POST a match + a commentary event with `curl` while watching a `wscat -c ws://localhost:8000/ws` connection receive the broadcast. If the event arrives in the socket, you've exercised the entire real-time path end to end. Then, in `sportz-ui`, run `npm run test:e2e` and watch Playwright build a prod server on :3100 and drive the same path through the real browser: the front-to-back loop, both sides covered.
</Note>
