Cloudflare Worker backend with static frontend (GitHub Pages). No shared secret, no Google sign-in. Identity established by per-device session tokens.
- User links on a device → backend mints UUID token → stored in D1
sessionstable → client persists inlocalStorage - Every request sends token → backend resolves to linked player
- Stale sessions lazily deleted on next use
- 90-day rolling TTL from
last_activetimestamp
- Email + player name from public roster
- One email ↔ one player — enforced at link time
- One player ↔ one email — changing requires admin
- Re-linking same email on new device creates new session, reuses player
- One vote per player per week —
UNIQUE(season_id, week, player_id)onvotestable - Both fields mandatory — leader and opponent, server-side validated
- Self-vote prohibited — opponent_id ≠ player_id
- Editable while open —
updateVotereplaces both fields, re-checks constraints - Privacy invariant — no endpoint exposes voter→opponent mapping; only aggregate tallies
Lint-style test (test/privacy/privacyGuard.test.js) asserts:
- No handler returns
opponent_idalongside voter identity in the same response getAppDatadoes not exposeopponent_idexcept incurrentVote(own data)
- Require
adminTokenin request body matchingADMIN_SECRETenv var - Constant-time comparison — prevents timing side-channel
- Actions:
startNewSeason,syncNow,pauseCurrentSeason,resumeCurrentSeason,materializePastAwards, leader management
- Best-effort per Worker isolate (30 requests/minute per IP)
- Documented, not a security guarantee
Backend trusts asserted email. For casual league use, accepted. Mitigations:
- Mandatory one-time link before voting
- One vote per player per week (UNIQUE constraint)
- Full audit logging (Cloudflare Worker logs)
- Unlink/revoke: user can unlink device; admin can unclaim player
ADMIN_SECRETencrypted in Cloudflare dashboard (not inwrangler.toml)- Never logged, never returned in responses
- Used only for constant-time comparison in admin actions