Scorepad
Source catalog · Knowledge graph
Facts
| Fact | Value |
|---|---|
| Status | active |
| Hosting platform | cloudflare |
| Repository | https://github.com/meta4m/scorepad |
| Tech stack | language: typescript, ui: react, routing: react-router, api: hono, validation: zod, styling: tailwindcss, build_tool: vite, testing: vitest, hosting: cloudflare-pages, runtime: cloudflare-pages-functions, database: cloudflare-d1 |
| Tags | scrabble, tournament, scoresheet, round-robin, standings |
| Last audited | 2026-09-15 |
| Upstream dependencies | scrabble-scoresheet |
| Downstream dependencies | None declared |
Secrets manifest
Key names and metadata only. Encrypted records are available in this project’s vault below.
| Key | Description | Environments | Required |
|---|---|---|---|
SESSION_SECRET | HMAC secret signing stateless per-sheet seat tokens (p1/p2/scorer claims on /game/:id); production falls back to a weak default if unset, so it must be set | production | Yes |
DIRECTOR_PASSWORD | Bootstrap password that creates the first director account on empty-database login; optional alternative to the /api/auth/setup first-run flow | production | No |
Overview
Project Overview: Scorepad
1. Executive Summary
- Mission: A multi-device Scrabble tournament manager — a director console for tournaments, divisions, rosters and pairings; per-player scoresheets on each player’s own phone; automatic reconciliation of independent sheets into official results.
- Target Audience: Tournament directors and scorekeepers running local Scrabble tournaments; players following live standings.
- Primary Maintainer: The operator (meta4m) — private project.
2. Current Status & Roadmap
- Lifecycle State:
active— phase-driven development (visible history: Phase 9 player registry → Phase 10 round-robin simplification/kickoff → Phase 10.1 absent players → paper-grid scoresheet rework, all on 2026-08-24; 8 D1 migrations). No wrap-up or deprecation signal in the repo. - Ongoing work: The newest commits reworked the scoresheet UI to mirror the original paper grid (
docs/scrabble_scoresheet.pdf) with score-only entry and a stable layout — UX iteration is in flight.
3. Core Capabilities
- Director console (
/admin): create tournaments/divisions, player registry with contacts (phone/email/club), Swiss or Berger round-robin pairing, kickoff times, editable tournaments, archive lock, absent-player handling without deleting results. - Scorekeeper access codes: tournament-scoped sessions via access code (12 h TTL); directors get account login (PBKDF2-hashed, 7-day sessions).
- Per-player scoresheets (
/game/:gid): seat tokens for p1/p2/optional scorer; turn grid; 100-tile tracker with undo; WhatsApp/email/copyable match-code handoff (PK phone normalization); first 30-second start countdown gate; first-mover seat swap until first turn. - Reconciliation: independent per-owner sheets compared; agreement confirms the official result, mismatch flags
needs_reviewfor director adjudication. - Standings: points/spread/rating-sorted tables with bye = win +50 spread; public read-only tournament view (
/t/:id) and printable blank scoresheet (/print). - Successor to scrabble-scoresheet: it implements that project’s documented roadmap (multi-device sync via D1, match pairing, two-sheet reconciliation, tournament management) and ships the identical source paper scoresheet in
docs/. Training-side needs live in the sibling app scrabbler.
4. Key References
- Repository: meta4m/scorepad
- Production URL:
https://scorepad.pages.dev— referenced intests/phase9.test.tsshare links and thescorepadPages project name; live deployment is not independently confirmed in-repo (no CI workflow file exists; deploys are manual). - Architecture Docs:
architecture.md· Deployment Guide:deployment.md
Architecture
Architecture & System Design: Scorepad
1. System Topology
graph TD Director[Director browser] --> Admin[React admin console /admin<br/>tournaments · rosters · pairings] Keeper[Scorekeeper] --> Code[Access-code login /api/auth/code] Player[Player phone] --> Sheet[Scoresheet /game/:gid<br/>seat token p1/p2/scorer] Public[Players / spectators] --> Pub[Public standings /t/:id] Admin --> SPA[React 19 + Tailwind SPA<br/>Cloudflare Pages] Sheet --> SPA Pub --> SPA Code --> API SPA --> API[Hono API on Pages Functions<br/>functions/api catch-all] API --> D1[(D1 scoresheet-db<br/>8 migrations)]2. Core Components & Subsystems
- SPA (
src/): React 19 + React Router 7 + Tailwind 4, built by Vite 6; pages for Home, Login, admin layout (Tournaments, Players, TournamentDetail, Division), Scoresheet, PublicTournament, BlankScoresheet;useAuthsession hook;api.tstyped fetch client;share.tsmatch-code handoff. - API (
server/): Hono app mounted at/apivia the Pages Functions catch-all route infunctions/api/(hono/cloudflare-pages); routesauth,players,tournaments,entries,games,sheets;/api/healthchecks D1 liveness; zod-validated JSON bodies. - Domain logic (pure, shared):
pairings.ts(Berger round-robin + Swiss with rematch avoidance),standings.ts(points/spread with bye +50),reconcile.ts(compare p1/p2/scorer sheet totals),registry.ts,joincode.ts;shared/holdstiles.ts(100-tile distribution),startGate.ts(30 s countdown + seat-swap freeze),sheet.ts,types.tsused by server, client, and tests alike. - Auth subsystem (
server/auth.ts): PBKDF2 password hashing (Web Crypto), D1-backed sessions withdirector/scorekeeperroles, HMAC-signed stateless seat tokens (gameId.owner.sig) for per-sheet claims; client stores seat claims inlocalStorage(sheetClient.ts). - Storage: D1
scoresheet-db—users,sessions,tournaments,divisions,players,entries,games(join codes, needs_review, starts_at),turns,sheets(migration 0004+).
3. Data Flow & Storage Contracts
- Primary storage: Cloudflare D1 (
DBbinding,scoresheet-db, migrations 0001–0008); server is the single source of truth for tournaments/games/turns/sheets. - Client storage:
localStorageholds only seat-token claims per game (sp_*keys) so a player re-enters their own sheet without a login. - Sheet contract: each game can have independent sheets for
p1,p2, and an optionalscorer; turns are{turn_no, word, score, total_after}rows (max 25 per side); reconciliation requires all present sheets to agree on final totals before a game confirms. - Tests: Vitest suites for pairings, reconcile, roster, and phase-9 flows.
4. Architectural Decision Records (ADRs)
ADR-01: Hono on Pages Functions — one deployment unit
- Context: A React SPA plus an API, with an aversion to separate Worker services.
- Decision: The Hono app runs as a Pages Function catch-all (the
functions/apiroute handler); static assets and API deploy together todist/. - Consequences: Single
wrangler pages deployships everything; API scaling is bounded by Pages Functions (no separate routing, queues, or cron).
ADR-02: Pure shared domain modules
- Context: Pairings, standings, start-gate, and tile rules must behave identically on server, client, and in tests.
- Decision: Pure functions in
server/andshared/with no DB or DOM access; routes only orchestrate persistence. - Consequences: High unit-test coverage without mocks; the trade-off is duplicated import graphs kept honest by
tsconfig.functions.json.
ADR-03: Keep the two-independent-sheets principle from scrabble-scoresheet
- Context: Tournament dispute-avoidance practice (each player keeps their own sheet).
- Decision: Per-owner sheets with mandatory p1/p2 confirmation; disagreement sets
needs_reviewfor director adjudication instead of auto-averaging. - Consequences: Trust model survives digitization; reconciliation adds a manual adjudication step on mismatch.
ADR-04: Passwords and seat tokens via Web Crypto, no auth service
- Context: No third-party identity provider wanted; players must join a sheet with zero accounts.
- Decision: PBKDF2 (100k iterations) hashes for directors; random-token D1 sessions with role + tournament scoping; HMAC seat tokens as stateless per-sheet claims; optional
DIRECTOR_PASSWORDbootstrap on an empty database. - Consequences: No external auth dependency;
SESSION_SECRETbecomes production-critical (weak fallback secret would forge seat tokens).
Deployment
Deployment & Operational Runbook: Scorepad
1. Hosting & Environments
| Environment | Platform | URL / Endpoint | Auto-Deploy Branch |
|---|---|---|---|
| Production | Cloudflare Pages | https://scorepad.pages.dev (project name in wrangler.toml; referenced by test share links — live status unverified) | none — manual npm run deploy |
| Local dev | Vite build —watch + Wrangler Pages dev | http://localhost:8788 | N/A |
Note: unlike scrabbler and scrabble-scoresheet, no GitHub Actions workflow exists in this repo — every deploy is manual.
2. Prerequisites & Toolchain
- Node.js + npm; Wrangler 4.x (
devDependency) with an authenticatedwrangler login. - Cloudflare account holding the
scorepadPages project and thescoresheet-dbD1 database (database_idis pinned inwrangler.toml).
3. Local Development Setup
git clone https://github.com/meta4m/scorepadcd scorepadnpm installnpm run db:migrate # wrangler d1 migrations apply scoresheet-db --localnpm run dev # vite build --watch + wrangler pages dev dist --port 8788First run locally: hit /api/auth/state — needs_setup: true opens the first-run director setup (/api/auth/setup), or set DIRECTOR_PASSWORD in .dev.vars (gitignored) and bootstrap via login.
4. Environment & Secrets Provisioning
Refer to spec.yml for declared secret keys.
- Secret Injection: Retrieve ciphertext from 2Brain vault and decrypt locally into
.env.local(.dev.varsfor wrangler). - Runtime (Pages project):
SESSION_SECRET(seat-token HMAC — required in production; a weak default fallback exists for dev only) and optionalDIRECTOR_PASSWORD(director bootstrap). Set withwrangler pages secret put <KEY> --project-name scorepad. No other external credentials exist.
5. Deployment Commands
npm run build # typecheck both configs + vite build → dist/npm run deploy # wrangler pages deploy dist --project-name scorepad# D1 schema changes are never applied by deploy — run explicitly:npx wrangler d1 migrations apply scoresheet-db --remote6. Verification & Health Checks
curl -f https://scorepad.pages.dev/api/health→{"status":"ok","service":"scorepad","db":"up"}(reports D1 liveness).- Smoke test: director creates tournament + division + players → generate pairings → open a game’s share link → enter codes on two devices → enter turns on both sheets → confirm both → game final and standings update on
/t/:id. - Pre-deploy gates:
npm run typecheck,npm run lint,npm testall clean.
7. Incident Response & Rollback
- Rollback: redeploy the previous git revision —
git checkout <good-sha> && npm run build && npm run deploy. D1 migrations are forward-only (SQLiteALTERlimits mean some changes rebuild tables), so a schema rollback requires a new corrective migration, not a redeploy. - Seat-token forgery concern: rotate
SESSION_SECRET(wrangler pages secret put SESSION_SECRET) — old seat tokens become invalid; players rejoin via match codes. - Mismatched sheets mid-tournament: games flagged
needs_revieware adjudicated from the director console (reviewis a flag, not a status value — SQLite CHECK constraint workaround).
Project vault
Cloudflare Access authorization and your passphrase are required to retrieve and decrypt stored values.
Client-Side Encrypted Secret Vault
Zero-knowledge architecture. Secrets are encrypted client-side using Web Crypto AES-256-GCM before leaving this browser tab. Edge databases store only ciphertext, IV, and salt.
Encrypted Records for scorepad (0)
Encrypted records have not been loaded.