Skip to content

Repository files navigation

MacaQuiz

Realtime multiplayer quiz game built with Next.js and Socket.IO.

Local Run

  1. Install dependencies:
npm install
  1. Configure environment variables (see below).
  2. If you are migrating an existing repo that still has data/quizzes/*.json or data/quiz-governance/*.json, import them once into SQLite:
npm run migrate:quiz-storage
  1. Start the app on the smoke-test port:
npm run dev -- --port 31001
  1. Open http://127.0.0.1:31001.

Environment Variables

  • ADMIN_SESSION_SECRET:
    • required for /api/admin/session (signed admin cookie sessions used by /add)
  • ADMIN_SESSION_TTL_SECONDS:
    • optional admin session lifetime (default 43200, 12h)
  • ADMIN_VIEWER_TOKEN, ADMIN_EDITOR_TOKEN, ADMIN_ADMIN_TOKEN:
    • recommended machine-token credentials mapped to roles/capabilities
    • accepted as Authorization: Bearer <token> or x-admin-token
  • OPS_ADMIN_TOKEN:
    • machine-token fallback for /api/debug/status (ops:read)
  • QUIZ_BUILDER_ADMIN_TOKEN:
    • machine-token fallback for /api/quiz-builder (quiz:read + quiz:mutate)
  • QUIZ_DB_PATH:
    • optional SQLite file for canonical quiz storage (default data/quizzes.sqlite)
    • runtime access uses node:sqlite on Node 24+
  • DEBUG_ADMIN_TOKEN:
    • broad fallback machine token for dev/debug only
  • REDIS_URL:
    • Redis store/adapter URL for room state and multi-instance scaling
    • optional only in local development mode (memory fallback)
    • required when NODE_ENV is not development (startup/runtime throws if missing)
  • NEXT_PUBLIC_SITE_URL:
    • canonical public site URL used by page metadata (default production URL: https://macaquiz.com)
  • ADMIN_MUTATION_RATE_LIMIT, ADMIN_MUTATION_RATE_WINDOW_MS:
    • mutation API rate-limit policy for /api/quiz-builder writes
  • PLAYER_SESSION_TTL_SECONDS:
    • player account session lifetime in seconds (default 2592000, 30 days)
  • PLAYER_HISTORY_MAX_ENTRIES:
    • max persisted match-history entries per player identity
  • ROOM_JOIN_FAILURE_WINDOW_MS, ROOM_JOIN_LOCK_THRESHOLD, ROOM_JOIN_LOCK_BASE_MS, ROOM_JOIN_LOCK_MAX_MS:
    • brute-force protection policy for invalid room:join attempts
  • DEBUG_SOCKET=1:
    • enables socket debug logs
  • LOG_LEVEL:
    • structured log threshold (debug, info, warn, error)
  • OPS_METRICS_WINDOW_MS:
    • rolling window used by status metrics aggregation
  • ANALYTICS_ENABLED, ANALYTICS_STRICT_PRIVACY, ANALYTICS_WINDOW_MS, ANALYTICS_HASH_SALT:
    • product analytics controls (funnel/session/content aggregates)
    • strict privacy mode hashes actor identifiers before aggregation
  • SLO_MIN_SAMPLE_SIZE, SLO_ROOM_JOIN_ERROR_RATE_MAX, SLO_ANSWER_ERROR_RATE_MAX, SLO_RECONNECT_RESTORE_RATE_MIN, SLO_TRANSITION_LAG_P95_MS_MAX, SLO_TICK_LAG_P95_MS_MAX, SLO_COMPLETION_RATE_MIN:
    • service-level objective thresholds published in /api/debug/status
  • DRAIN_DEFAULT_GRACE_MS:
    • default graceful drain window for websocket node rollout
  • ROOM_TTL_SECONDS, TOKEN_TTL_SECONDS, LOCK_RETRY_COUNT:
    • optional room/store tuning knobs
  • ROOM_TICK_INTERVAL_MS, ROOM_SCHEDULER_LEASE_MS:
    • optional realtime scheduler tuning for multi-instance deployments
  • ROOM_TICK_MIN_DELTA_MS, ROOM_TICK_COALESCE_MS, ROOM_TICK_OVERLOAD_SOCKET_COUNT, ROOM_TICK_OVERLOAD_MIN_INTERVAL_MS:
    • tick backpressure tuning (coalescing + stale drop strategy for large rooms)
  • PHASE_BROADCAST_DEDUPE_SECONDS:
    • short cross-node dedupe window for game:phase broadcasts
  • PERF_BUDGET_JOIN_ACK_P95_MS, PERF_BUDGET_JOIN_ACK_P99_MS, PERF_BUDGET_ANSWER_ACK_P95_MS, PERF_BUDGET_ANSWER_ACK_P99_MS, PERF_BUDGET_PHASE_LAG_P95_MS, PERF_BUDGET_PHASE_LAG_P99_MS:
    • load/performance gate thresholds consumed by npm run perf:gate
  • QUIZ_CACHE_CHECK_INTERVAL_MS, QUIZ_CACHE_REDIS_CHECK_INTERVAL_MS, QUIZ_CACHE_REDIS_KEY:
    • quiz metadata/content cache invalidation tuning

Architecture Map

Canonical active runtime

  • Socket gateway: src/pages/api/socket.ts
  • Room/game engine: src/lib/rooms/*
  • Identity/profile services: src/lib/identity/*
  • Quiz read APIs: src/app/api/quizzes/*
  • Player/host routes: src/app/page.tsx, src/app/create/page.tsx, src/app/join/page.tsx, src/app/room/[code]/page.tsx
  • Identity UI routes: src/app/account/page.tsx, src/app/profile/page.tsx
  • Client socket layer: src/lib/socket/*
  • Canonical quiz content:
    • SQLite: data/quizzes.sqlite
    • images: public/images/quizzes/*
    • governance + version history: SQLite tables inside the same database

Compatibility and legacy boundaries

  • Legacy URL redirects kept only for compatibility:
    • src/app/room/new/page.tsx
    • src/app/room/join/page.tsx
    • src/app/room/[code]/play/page.tsx
  • Legacy app-game runtime modules (old components/state model) were removed in Phase 0 to avoid accidental imports.
  • /api/quiz-builder and /add now read/write canonical content only:
    • data/quizzes.sqlite
    • public/images/quizzes/*
  • Legacy source trees are migration-only inputs:
    • src/app/lib/data/games/*
    • public/images/games/*
  • CI enforces a drift check from legacy inputs to canonical outputs via:
    • npm run check:content-drift

Canonical Content Rules

  • Runtime and builder source of truth:
    • quiz records live in data/quizzes.sqlite
    • quiz images must live in public/images/quizzes/<quizId>/*
  • Existing canonical JSON migration:
    • import current canonical quiz + governance JSON into SQLite:
      • npm run migrate:quiz-storage
    • optionally remove the old JSON files after a successful import:
      • npm run migrate:quiz-storage -- --delete-json
  • Canonical quiz schema:
    • QuizV2 with { id, title, version: 2, questions[] }
    • questions[] uses { id, prompt, options[], correctOptionId, image? }
  • Legacy-to-canonical migration:
    • dry-run: npm run migrate:quizzes -- --dry-run
    • apply: npm run migrate:quizzes
    • optional move semantics: npm run migrate:quizzes -- --move
    • target is SQLite (data/quizzes.sqlite), not data/quizzes/*.json

Quiz Lifecycle and Governance (phases-1 Phase 4)

  • Lifecycle states:
    • draft
    • in_review
    • published
    • archived
  • Runtime visibility:
    • /api/quizzes, /api/quizzes/[id], and gameplay quiz selection serve published quizzes only.
  • Governance persistence:
    • SQLite tables inside data/quizzes.sqlite
    • one table for lifecycle records
    • one table for immutable version snapshots
  • Admin lifecycle actions:
    • endpoint: PATCH /api/quiz-builder
    • actions:
      • submit_review, request_changes, publish, archive, restore_draft
      • rollback (promote previous immutable version)
      • comment, flag, resolve_flag
  • Role gates:
    • editor/admin: review, publish, comments, flags
    • admin: archive, restore draft, rollback, resolve flag
  • Listing/filtering:
    • GET /api/quiz-builder?lifecycle=1
    • GET /api/quiz-builder?lifecycle=1&state=draft|in_review|published|archived

Gameplay Modes and Room Governance (phases-1 Phase 3)

  • Start readiness:
    • host can configure minPlayersToStart in the lobby settings
    • game start is blocked until at least that many connected player roles are present
  • Late join behavior:
    • policies:
      • next_question: join now, answer from next question
      • spectator: join mid-game as spectator
      • blocked: deny mid-game joins
    • policy is enforced server-side by room engine
  • Spectator role:
    • spectators receive room/game snapshots and leaderboard updates
    • spectators cannot submit answers or vote rematch
  • Team mode:
    • host can enable team mode and choose teamCount (2-4)
    • host can assign players to teams in lobby (team:assign)
    • scoring remains per-player and contributes to aggregated teamStandings
  • Room privacy and join governance:
    • roomPrivacy: public, private, invite_only
    • invite-only rooms require matching inviteCode in room:join
    • optional hostApprovalRequired join workflow with room:reviewJoinRequest
  • End-of-match continuity:
    • finished games support rematch voting (game:rematchVote)
    • rematch auto-starts when all connected active players vote yes
    • host can quick reset to lobby while preserving lobby settings (game:quickReset)
  • Host migration:
    • if the current host disconnects past grace period, host role moves to the next connected player
    • clients show handover messaging so both host and players understand who is now in control

Identity and Progression (phases-1 Phase 2)

  • Identity/session model:
    • guest users remain token-based and can play without account friction
    • authenticated users can register/sign in via /api/player/session and persistent cookie sessions
    • guest token identities can be upgraded/mapped to authenticated identities
  • Profile and preferences:
    • /api/player/profile supports read/update for display name, avatar, and language
    • UI entry points: /account and /profile
  • Match history and baseline stats:
    • room engine persists per-player completed-match summaries when games finish
    • /api/player/history returns recent matches and baseline progression stats

Performance and Scale (Phase 5)

  • Quiz metadata/content caching:
    • /api/quizzes and /api/quizzes/[id] use shared in-process quiz cache
    • cache refresh checks SQLite row revisions and reparses only changed quizzes
    • optional Redis revision coordination is enabled automatically when REDIS_URL is set
  • Realtime scheduler hardening:
    • ticker default interval is 250ms and uses per-room distributed scheduler leases to reduce multi-node duplicate ticking
    • timed phase transitions are ticker-driven to keep phase progression deterministic
    • phase broadcast dedupe window reduces duplicate game:phase emissions across nodes
  • Realtime hot-path optimization:
    • room broadcast flushes are coalesced per room (snapshot / players / phase) to avoid redundant reads/emits under bursty event traffic
    • reveal results are computed in one store read (getRevealResults) and emitted per socket without per-socket room snapshot fetches
    • leaderboard emissions precompute rank/team lookup maps to avoid repeated find scans per socket
  • Backpressure controls:
    • tick emissions are coalesced, stale ticks are dropped, and volatile emits are used to avoid websocket queue buildup
    • large-room tick throttling is enforced when socket fan-out crosses ROOM_TICK_OVERLOAD_SOCKET_COUNT
    • /api/debug/status now includes metrics.backpressure counters (drop totals and reason breakdown)
  • Load tooling and budget gates:
    • scripts/load-rooms.mjs now supports many-rooms, fan-out, and reconnect-burst profiles with p50/p95/p99 metrics
    • scripts/perf-gate.mjs enforces latency/skew budgets from a JSON load report
    • CI smoke job runs a reduced perf profile and fails on budget regressions
  • Image delivery:
    • active gameplay/preview images use Next image optimization path
    • quiz image routes now publish explicit cache-control headers (/images/quizzes/*)

Capacity Envelope

  • Validated profile envelopes (single node, local baseline):
    • many-room: 6 rooms x 6 players (36 active players)
    • fan-out: 1 room x 16 players
    • reconnect burst: 3 rooms x 8 players with 4 concurrent reconnects per room
  • Recommended production starting envelope per websocket node:
    • <= 16 players per room for interactive games
    • <= 30 active rooms per node at 6 players/room average (180 concurrent players)
  • Scaling assumptions:
    • Redis room store + Socket.IO Redis adapter enabled
    • sticky websocket routing at the edge/load balancer
    • tune room count/player cap upward only after npm run perf:check passes against production-like hardware

Operations (Phase 4)

  • Structured logs:
    • room lifecycle, phase transitions, reconnects, and key error paths emit JSON logs
    • sensitive fields are redacted (token, authorization, secret, etc.)
  • Ops status endpoint:
    • /api/debug/status exposes active rooms/players, reconnect rates, socket event error rates, and tick/transition lag
    • access is protected with RBAC ops:read credentials (session or machine token)
  • Admin session endpoint:
    • /api/admin/session provides authenticated admin cookie session for /add
    • roles: viewer, editor, admin
  • Documentation:
    • runbook: docs/operations/runbook.md
    • release checklist: docs/operations/release-checklist.md
  • CI quality gates:
    • .github/workflows/ci.yml enforces lint, tests, build, smoke checks, and perf budget gate

Reliability and Analytics (Phase 7)

  • Extended status diagnostics:
    • /api/debug/status now includes:
      • analytics (privacy-aware funnel/session/content aggregates)
      • slo (objective-level health and thresholds)
      • drain (graceful websocket drain state and active socket count)
  • Node drain controls:
    • GET /api/debug/drain (read)
    • PATCH /api/debug/drain (admin role required) for deploy-time graceful websocket draining
  • Release safety scripts:
    • npm run slo:check (SLO gate)
    • npm run canary:check (canary promote/rollback gate)
  • Resilience scripts:
    • npm run chaos:run (disconnect storm, jitter, Redis degradation simulation hooks)
    • npm run soak:rooms (long-running room stress loop with optional SLO fail-fast)
  • Phase 7 docs:
    • docs/operations/slo-analytics.md
    • docs/operations/runbook.md
    • docs/operations/release-checklist.md
    • docs/operations/incident-template.md

Smoke Checks

Run with the app listening on http://127.0.0.1:31001:

node scripts/smoke-socket.mjs
node scripts/smoke-multiplayer.mjs

Optional explicit endpoint:

SOCKET_URL=http://127.0.0.1:31001 node scripts/smoke-socket.mjs
SOCKET_URL=http://127.0.0.1:31001 node scripts/smoke-multiplayer.mjs

Load and Perf Checks

Run with the app listening on http://127.0.0.1:31001:

npm run load:rooms
npm run perf:check
npm run bench:quiz-api
npm run slo:check
npm run canary:check
npm run chaos:run
npm run soak:rooms

Optional tuning:

SOCKET_URL=http://127.0.0.1:31001 LOAD_PROFILE=many-rooms LOAD_ROOMS=8 LOAD_PLAYERS_PER_ROOM=8 npm run load:rooms
SOCKET_URL=http://127.0.0.1:31001 LOAD_PROFILE=fan-out LOAD_FANOUT_PLAYERS_PER_ROOM=20 npm run load:rooms
SOCKET_URL=http://127.0.0.1:31001 LOAD_PROFILE=reconnect-burst LOAD_RECONNECT_ROOMS=4 LOAD_RECONNECT_PLAYERS_PER_ROOM=10 npm run load:rooms
SOCKET_URL=http://127.0.0.1:31001 LOAD_PROFILE=all LOAD_REPORT_PATH=artifacts/perf/load-report.json npm run load:rooms
PERF_REPORT_PATH=artifacts/perf/load-report.json npm run perf:gate
SOCKET_URL=http://127.0.0.1:31001 QUIZ_BENCH_ITERS=50 npm run bench:quiz-api
OPS_ADMIN_TOKEN=<token> SLO_BASE_URL=http://127.0.0.1:31001 SLO_REQUIRE_REDIS_CONNECTED=0 npm run slo:check
OPS_ADMIN_TOKEN=<token> CANARY_BASE_URL=http://127.0.0.1:31001 CANARY_DURATION_MS=15000 CANARY_REQUIRE_REDIS_CONNECTED=0 npm run canary:check
OPS_ADMIN_TOKEN=<token> CHAOS_BASE_URL=http://127.0.0.1:31001 npm run chaos:run
OPS_ADMIN_TOKEN=<token> SOAK_BASE_URL=http://127.0.0.1:31001 SOAK_DURATION_MS=180000 npm run soak:rooms

Verification Commands

npm run lint
npm run test
npm run check:content-drift
npm run build
node scripts/smoke-socket.mjs
node scripts/smoke-multiplayer.mjs
npm run load:rooms
npm run perf:check
npm run slo:check
npm run canary:check
npm run chaos:run

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages