Groundball
Groundball is a stat-tracking app for live girls’ lacrosse games. It’s designed for the person in the stands holding a phone one-handed in bright sunlight with terrible cell signal. Goals, assists, ground balls, saves, and six other stat types are recorded with a single tap, and the data syncs to a per-player season record that produces sparkline trends, personal bests, and a printable yearbook page at the end of the year.
Why it exists
My daughter’s high school coach asked for help tracking stats this season. We tried paper, but it was slow and capturing enough detail meant that we stopped watching the game and were instead trying to manage rapid data entry. We looked at stats apps, but the existing options were either built for coaches (heavy, multi-tap workflows that assume a tablet and a quiet bench) or were generic spreadsheet templates that fell apart the moment the network dropped. Neither fit the actual job.
I decided to build what I wanted instead. The constraints drove the design. Both cellular signal and wifi at our field are unreliable, so the app had to work fully offline and reconcile later. One hand holds the phone, so every primary action is a single tap with no modal and no confirmation.
The fun part of this kind of data capture (yes, I said “fun”) is what it allows us to do. Because the system is processing the data on the backend, we have instant access to per-game and full season stats, can track an invidual player’s progression in skill and performance, and dive deeper into the data. The data model is player-centric from the start.
How it was built
Groundball was built with Claude Code, like many of my other coding projects. The main difference for this one was the “offline-first” requirement. Plays write to Dexie.js (IndexedDB) first and queue for sync; the service worker is built with Serwist so the app shell loads even when the device is offline. The end-game lock auto-retries for 60 seconds when plays are still syncing, so closing out a game doesn’t drop data.
How it works
Live Entry
The scoring grid is the heart of the app. Each critical stat gets a button on a single screen: tap the stat, then tap the player’s number on the second screen to record it. No convoluted entry. The only play that gets extra steps is a goal, which prompts for the assist data as well. An undo toast appears for eight seconds in case of a miss-tap. A sync status pill at the top of the screen shows pending, synced, offline, or failed at a glance, with one-tap retry when sync stalls.
Before the game, attendance is marked so absent players are hidden from the player selector. Afterward, the post-game screen shows the final score and detailed per-player stats. The scorer can choose to share a publicly-accessible URL with player last names redacted to just their initials.
Per-player season pages
Every play eventually rolls up into a per-player season page: sparkline trends across games, personal bests, improvement deltas vs. the prior period, and a game-by-game log. The season also produces a printable yearbook — one page per player with stats, a short narrative, and the game log — so the season ends with something physical to keep.
Sharing
Two kinds of share links are exposed. /g/[slug] renders a single game summary, and /s/[slug] renders a season page, both server-rendered and read-only. From the Live Entry screen, one tap copies a watch link so family who can’t be in the stands can follow along play by play. A CSV export of the season, including team totals, is available for any member who wants to take the data elsewhere.
Multi-user and roles
Groundball supports inviting other people (typically family members or co-scorers) via email, with three roles: owner, scorer, and view-only. Pending invites can be tracked and revoked. Live scoring uses a claim/release flow with atomic race protection, so two people can’t accidentally score the same game from two phones.
Stack
Next.js 16 App Router, React 19, TypeScript (strict), Tailwind CSS 4 with @theme inline tokens, Supabase (PostgreSQL with row-level security and Auth), Dexie.js for IndexedDB, Serwist for the service worker, Vitest for tests. Deployed on Vercel using the webpack build (required for Serwist). Source not public (yet).