← Project index

Product instinct and use

Spartan Stats

Receipts for Tuesday football.

The group chat needed evidence

After Tuesday football, everyone remembered a different top scorer, assist leader, or best run of form. Spartan Stats replaced memory with receipts. The group has used it since around April 2026, and the product has grown out of the arguments it settles.

Small product, real domain

A mobile-first session form records goal-by-goal scorer and assister pairs, forced own goals, attendance, and goalkeeper concessions. From those records the app derives monthly and all-time leaderboards, form, player profiles, streaks, chemistry, monthly awards, clean sheets, and rule-based Match Day Facts.

Leaderboards, awards, and individual facts can be copied in a WhatsApp-ready format. That is not a generic sharing feature. It meets the product where the group already talks.

The data model grew with the banter

First modelstatsgoals + assists per player and session
Richer sourcegoal_eventsscorer ↔ assister pairing
Derivedfactschemistry + streaks + milestones

The original aggregate row could answer “how many?” but not “with whom?” A versioned migration introduced goal_events as the richer source for chemistry and facts while keeping aggregate stats as a cache. Historical counts were expanded into events with an explicitly lossy, idempotent backfill because past scorer-assister pairings could not be recovered.

Useful fixes leave a paper trail

A September migration repairs sessions shifted from Tuesday to Monday by local-midnight toISOString() conversion in positive UTC offsets. It updates only Monday records without an existing Tuesday collision. Another migration models “Own Goal” as a hidden system player, preserving ordinary leaderboard behavior while still crediting the player who forced it.

Tradeoff: the session form currently writes event detail and aggregate rows. That keeps existing reads simple but makes write-path consistency an explicit concern.

Evidence in the open