Here's a failure mode I was determined to avoid: the season page says a player shoots 62%, the live view says 58%, and the native app says 60% — all for the same player, same games. Three numbers, three slightly different implementations, zero trust.

This is post #3 in my build-in-public series about SportsFlow. The fix was to make the analytics a single thing that runs in all three places.

The shape: a pure-compute package

There's a package, @sportsflow/analytics, with one hard rule: no database dependencies, no DOM dependencies. It's just functions. Events in, numbers out.

match_event rows ──adapter──▶ AnalyticsEvent[] ──compute──▶ KPIs / heatmaps / momentum