WebSockets
- Beginner. A persistent two-way connection between browser and server. Unlike HTTP (request → response → done), it stays open so the server can push data without being asked.
- Intermediate. Sportz shares one HTTP server between Express and
wsvia anupgradelistener. Broadcast scope follows visibility:commentarygoes to a match’s room;match_createdandscore_updatego to all clients (they change the card shown in every grid). See ADR-010. - Senior. Why not SSE or polling? SSE is one-way (no client→server subscribe); polling is high-latency and wasteful. We hand-rolled rooms/heartbeat/reconnect instead of Socket.IO to keep the dependency surface small, accepting that we own that code. (See ADR-002.)
- Staff. The subscription registry is in-process memory. At multi-instance scale a client on instance A misses events from instance B; you need a Redis pub/sub backbone. Know when that becomes mandatory (horizontal scaling) vs premature (System Architecture).
TanStack Query
- Beginner. A library that fetches and caches server data, giving you loading/error states for free.
- Intermediate. Sportz keys queries by
['matches']and['commentary', matchId].enabled: matchId !== nullstops the commentary query firing until a match is selected. - Senior. The key move: WebSocket events write into the same cache via
setQueryData, so REST and live data share one source of truth, no reconciliation layer. Replaced ~50 lines of hand-rolleduseState/useEffect/AbortController per hook. (ADR-003.) - Staff. Know the cache-invalidation model and
staleTimetradeoffs; understand whyrefetchOnWindowFocusis off here (WS already keeps data live, so refocus refetches would be redundant work).
Docker & multi-stage builds
- Beginner. Packages your app + its environment into one portable image that runs the same everywhere.
- Intermediate. Sportz uses a
builderstage (compiles TypeScript) and arunnerstage (ships onlydist/+ prod deps, runs as non-root). The final image has no compiler or source. - Senior. Why multi-stage? Smaller image, lower attack surface, no devDeps in production. Why a non-root user? Limits blast radius, but it means runtime-written paths need explicit
chown(a real crash, ISSUE-002). - Staff. Layer caching strategy (copy
package*.jsonbefore source sonpm cicaches), multi-arch builds (amd64 + arm64), and the dev/prod parity story (Neon Local vs Neon Cloud).
TypeScript
- Beginner. JavaScript with types checked before the code runs.
- Intermediate. Sportz shares data shapes across backend and frontend; the frontend mirrors Drizzle-inferred types.
- Senior. The payoff is concentrated at boundaries (the WebSocket message protocol and API contracts), where a wrong shape is easy to introduce and expensive to debug at runtime. (ADR-001.)
- Staff. Trade strictness against velocity; know where types earn their keep (contracts, public APIs) vs where they’re ceremony.
Drizzle + Neon
- Beginner. Drizzle is a type-safe query builder; Neon is serverless Postgres.
- Intermediate. Schema in
schema.ts, parameterized queries, FK cascade fromcommentary→matches. Neon Local forks an ephemeral branch per dev run. - Senior. The SSL strategy has three cases (Cloud verified / Local self-signed / plain Postgres off):
ssl: false≠{ rejectUnauthorized: false }(a real bug, ISSUE-003). Pooled vs direct connections matter for serverless. - Staff. Connection-pool sizing under serverless, migration strategy in CI/CD, and read-replica/caching decisions at scale.
Observability (New Relic / PostHog)
- Beginner. New Relic tells you is it healthy, how fast (errors, performance, traces); PostHog tells you what are users doing (analytics, funnels).
- Intermediate. New Relic’s Node agent auto-instruments
express/pg/httpwith no manual instrumentation; on the frontend it’s lazy-loaded after hydration so it doesn’t hurt the LCP metric it measures. PostHog is wired on both sides:posthog-js(frontend autocapture + custom events) andposthog-node(backend business events). - Senior. Three pillars: logs, metrics, traces. Sportz has all three live on both backend (Winston + New Relic APM) and frontend (New Relic Browser RUM/traces). (Observability.)
- Staff. Why New Relic instead of OpenTelemetry+Sentry: vendor lock-in traded for one connected trace across frontend and backend with no collector to run (ADR-013). Knowing what to measure, thresholds, and escalation once real traffic exists.
Testing (Vitest + Playwright)
- Beginner. Automated checks that run your code and fail loudly if it misbehaves, so you change things without fear. Vitest tests the backend; Playwright drives the real UI in a browser.
- Intermediate. Sportz layers them: unit (pure logic), integration (routes against a real Postgres), WebSocket (real socket pairs), and frontend E2E (Playwright with REST/WS mocked + an axe a11y scan). (Testing.)
- Senior. Mock what you don’t own, keep real what you verify (Arcjet mocked, DB real). Prefer user-facing locators and web-first assertions. The E2E suite runs against a production build on its own port:
next dev’s on-demand compilation causes flaky timeouts under parallelism. (ADR-008.) - Staff. Realness is a ladder (mocked → local backend → deployed smoke) via
PLAYWRIGHT_BASE_URL; don’t make one tier carry two jobs. Treat flakiness as a P-class bug:retriesabsorbs rare true-flake, it doesn’t excuse a structural one. Contract testing and visual regression are the next rungs.
Deployment & CI/CD
- Beginner. CI = automated checks on your code (lint, types, tests). CD = automatically shipping it. Sportz: backend → Render, frontend → Vercel.
- Intermediate. Push → GitHub Actions run the checks; a platform deploys on merge. The frontend’s
NEXT_PUBLIC_*config is baked at build time, so changing it needs a redeploy. (DevOps.) - Senior. “Git push = deploy” is a generic PaaS model (Vercel, Render, Netlify, Heroku), not Vercel-specific. CI and CD are independent unless you connect them: branch protection on
main(require checks + PR) is what makes CI gate production, because production deploys frommain. (ADR-009.) - Staff. Pick the deploy model per workload (managed PaaS vs own-pipeline/containers) by naming the dominant constraint: convenience vs control/compliance. Keep high-realness checks (deployed smoke) out of the per-push loop; automate them post-deploy. Branch protection turns “we run CI” into “red blocks merge.”