Real-time multiplayer football squares pools. Friends claim squares on a 10×10 grid; payouts go to whoever owns the cell whose row/column digits match the score at quarter-end. Built and run on Super Bowl Sunday for ~50 concurrent players.
▶ Live Demo — squares.nathankrebs.com
- Real-time grid sync — sub-100ms cross-client updates via Supabase broadcast + postgres_changes (see ADR-0003)
- Optimistic claims with rollback — taps feel instant; failed claims roll back with a toast (see ADR-0002)
- Custom teams — set team names, colors, and logos per party; configurable app-wide via env vars
- Future-game ready — name a specific event and optional kickoff; pools stay available through game day
- Editable setup before lock — hosts can correct event details, kickoff, and matchup without recreating a pool
- Multiple payout structures — Rising / Equal / Big Finish / Custom
- Host PIN protection for grid lock, score entry, payout edits, party deletion
- PWA installable on iOS + Android with push notifications
- Matchup-aware live score assist — links to ESPN-sourced NFL scores when the active game matches the party teams; manual scoring stays available as the fallback
- Color-coded player legend with click-to-filter
- Pan and zoom for usable squares on small phones
- WCAG 2.1 AA targets — ARIA grid markup, dialog modal with focus trap, semantic forms
| Layer | Choice |
|---|---|
| Frontend | SvelteKit 5 (runes in components, legacy stores in shared state — why) |
| Styling | Tailwind 4 + CSS variables |
| Language | TypeScript (strict, --max-warnings 0) |
| Backend | Supabase (Postgres + Realtime + RPC) |
| Hosting | Cloudflare Pages via @sveltejs/adapter-cloudflare — see DEPLOY.md |
| Observability | Sentry + Web Vitals (optional, no-op without DSN) |
| Testing | Vitest (unit + integration) + Playwright (e2e + visual regression) |
| CI | GitHub Actions — 5 jobs gating every PR |
Office and friend-group "squares" pools usually run via a paper grid + Venmo. Anyone joining late can't see the live grid. Anyone leaving early can't see who won which quarter. The host has to manually track scores against a paper-and-pen grid while paying attention to the actual game.
This app collapses all of that into a shared URL. Friends claim cells in real time, the host enters scores at quarter-end, the app computes winners and shows a payout summary. No accounts, no money flowing through the app — just the bookkeeping.
Ran live on Super Bowl Sunday 2026 with ~50 concurrent players. Zero downtime. Zero support requests.
This repo is intentionally shaped as a full-stack portfolio artifact, not just a weekend UI. The product constraint was a real event with non-technical users, spotty mobile networks, and a host who needed score entry to work while watching the game.
The senior-engineering work is in the operational details: a transactional create_party RPC, persisted event identity and event-aware retention for arbitrary future football games, source-of-truth Postgres changes paired with low-latency broadcasts, matchup-aware live score detection with manual fallback, optimistic claims with rollback, PIN-protected host actions, RLS hardening, PWA install support, optional Sentry/Web Vitals, and a game-day runbook. CI gates linting, formatting, type checks, coverage, build, bundle size, Supabase integration tests, and Playwright e2e coverage.
For reviewers, the fastest path is:
- Try the live demo from the home page.
- Read ARCHITECTURE.md for the system overview.
- Read ADR-0002 and ADR-0003 for the key realtime decisions.
- Read GAME-DAY.md for the production operations model.
- Node 22+
- A Supabase project (free tier works)
git clone https://github.com/nkrebs13/Squares.git
cd Squares
npm install
cp .env.example .env.local
# Edit .env.local: set VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY
npm run dev # http://localhost:5173Apply the migrations to your Supabase project:
# Option A: Supabase CLI (recommended)
supabase link --project-ref <your-ref>
supabase db push# Option B: SQL Editor — paste each file in numerical order
# supabase/migrations/001_*.sql through the latest numbered migrationImportant
Option B only: If you apply migrations via the SQL Editor instead of the CLI, also verify that Realtime replication is enabled for the parties, squares, numbers, scores, and winners tables in Supabase Dashboard → Database → Replication. The CLI applies 001_schema.sql (which contains the ALTER PUBLICATION supabase_realtime ADD TABLE … statements) automatically; the SQL Editor does not activate Realtime for you — the grid will appear to work but won't sync across clients in real time.
Running supabase db reset applies all migrations and seeds a demo party automatically. After reset, visit /party/DEMO01 (PIN: 0000) to see a partially-filled grid with four fictional players. To skip seeding, delete supabase/seed.sql before running reset.
Tip
The live demo at squares.nathankrebs.com is always running against a pre-seeded Supabase project. You can explore every game state without setting anything up locally.
Set PUBLIC_SENTRY_DSN in .env.local to send unhandled errors and Web Vitals (CLS, INP, LCP, FCP, TTFB) to a Sentry project. The free tier is sufficient for portfolio-level traffic; the app works identically with the variable unset.
PUBLIC_SENTRY_DSN=https://...@...ingest.sentry.io/...The repo ships with @sveltejs/adapter-cloudflare because Cloudflare Pages is the canonical production target (squares.nathankrebs.com runs there). Forks can still target Vercel, Netlify, or self-hosted Node by swapping the adapter as described in the deploy guide.
Full step-by-step instructions, env-var lists, custom-domain notes, and troubleshooting in docs/DEPLOY.md.
Brand strings, default event/team labels, and currency live in src/lib/config.ts and read from PUBLIC_* env vars at build time. To rebrand without touching code, set the relevant entries in .env.local (or your platform's env-var dashboard) before npm run build:
| Env var | Default |
|---|---|
PUBLIC_APP_NAME |
Football Squares |
PUBLIC_APP_URL |
https://squares.nathankrebs.com |
PUBLIC_APP_TAGLINE |
Football squares pools for any game |
PUBLIC_APP_DESCRIPTION |
Real-time football squares pools. … |
PUBLIC_DEMO_PARTY_CODE |
DEMO01 |
PUBLIC_DEFAULT_EVENT_NAME |
Football Squares |
PUBLIC_DEFAULT_TEAM_ROW_NAME |
Seahawks |
PUBLIC_DEFAULT_TEAM_ROW_COLOR |
#69BE28 |
PUBLIC_DEFAULT_TEAM_ROW_LOGO |
/logos/seahawks.png |
PUBLIC_DEFAULT_TEAM_COL_NAME |
Patriots |
PUBLIC_DEFAULT_TEAM_COL_COLOR |
#C60C30 |
PUBLIC_DEFAULT_TEAM_COL_LOGO |
/logos/patriots.png |
PUBLIC_CURRENCY_CODE |
USD (any ISO 4217 code) |
PUBLIC_LOCALE |
en-US (any BCP 47 locale) |
PUBLIC_APP_NAME and PUBLIC_APP_DESCRIPTION are also picked up by the PWA manifest in vite.config.ts. All values fall back to the defaults above when unset, so a stock fork keeps the Football Squares experience.
Note
Team names and colors can also be set per party from the create-party form — the env vars set the defaults pre-populated in the form.
The 10-minute orientation is in ARCHITECTURE.md. The deepest design decisions get their own ADRs:
- ADR-0001: Hybrid reactivity — why stores stay legacy and components use runes
- ADR-0002: Optimistic update chain — why
.then()instead ofawait, and what the 8 steps are - ADR-0003: Dual realtime channels — why both broadcast AND postgres_changes
Note
Each ADR documents the options considered, the tradeoffs weighed, and the decision made — not just what was built, but why. This is the fastest path to understanding the non-obvious design choices.
If you've cloned the repo and want to know "where does X live and why", that's the path.
For production operations, see GAME-DAY.md — the runbook used on Super Bowl Sunday, covering Supabase monitoring, emergency SQL for stuck party states, service worker cache clearing, and broadcast channel health diagnosis.
npm run dev # http://localhost:5173
npm run test # unit tests (Vitest)
npm run test:e2e # Playwright (Chromium + Mobile Chrome)
npm run lint && npm run check # quality gatesSee CONTRIBUTING.md for local Supabase setup, testing strategy, and contribution guidelines.
Please read our Code of Conduct before participating.
- Branch from
main - Run
npm run lint && npm run check && npm run testlocally - Open a PR; CI must be green before merge
MIT — see LICENSE.
Built by Nathan Krebs. Originally for Super Bowl 2026 with friends; now public as a portfolio piece.




