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

| 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:
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:5173
Apply 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 migration
[!IMPORTANT] 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, andwinnerstables in Supabase Dashboard → Database → Replication. The CLI applies001_schema.sql(which contains theALTER 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://[email protected]/...
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:
.then() instead of await, and what the 8 steps are[!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 gates
See CONTRIBUTING.md for local Supabase setup, testing strategy, and contribution guidelines.
Please read our Code of Conduct before participating.
mainnpm run lint && npm run check && npm run test locallyMIT — see LICENSE.
Built by Nathan Krebs. Originally for Super Bowl 2026 with friends; now public as a portfolio piece.