Skip to content

01Architecture

Posted

3 min read

By Wyatt McPherson

Nineteen leagues, one registry

Scorebug covers seven sports. Adding a league is mostly one entry in one file, but four other places still have to be edited by hand.

  • Architecture
  • PostgreSQL
  • Supabase
  • APIs

How do you build one app for many sports leagues without copying the code for each one?

Put everything that’s different about a league in one list and have the rest of the app read from it. In Scorebug, each league’s colours, score feed, news sources and labels come from that list through one lookup function, so all nineteen leagues run through the same code. A few things still live outside the list, and I treat each one as a bug to fix.

01

Every league is different

Every league’s data is a little different. Hockey has three periods and a shootout. Formula 1 has no clubs at all, so anything keyed on a team falls apart. Cricket is keyed by a numeric competition id where every other league uses a slug, its teams endpoint returns an empty array, and a date range in the query string returns a 404 while a single date works fine.

If you handle each difference where you find it, you end up with a switch statement on every screen. By the fourth league, adding a fifth means finding every one of them.

02

The registry

So each league gets one registry entry that carries everything that’s different about it, and every screen asks a single lookup function about the league it’s showing. Colours, ESPN configuration, whether date ranges are supported, news sources, surface treatments and display labels all come from that one place.

Adding a league is now mostly one entry. Four went in on a single change: the Chinese Super League, the Indian Super League, J.League and the Indian Premier League, sixty clubs between them. That took the app from fifteen leagues to nineteen.

03

What still gets edited by hand

It’s never quite one entry. Four things still have to be edited by hand: the raw team block, because club rosters are static data; the badge script and its dictionary, because crests are made ahead of time; and the news aggregation edge function.

The edge function is the one that caught me out. The aggregator runs on Deno inside Supabase and can’t import the browser-side registry, so it keeps its own list of leagues. Forget to add a new league there and everything looks right: the league appears, its scores load, its badges render. Only the news feed is empty, and nothing errors, because an empty result is a valid result.

04

An empty feed doesn’t look broken

An empty news feed looks the same as a quiet news day, so nothing told me it was broken. You only see the difference if you go looking, or if a user tells you. Nobody relies on the news feed yet, so nobody did.

It needs a check: either one at build time that the two lists match, or one at runtime that every league in the list gets at least one news source. A comment saying "remember to also update the edge function" didn’t stop me forgetting.

05

Where it stands

Nineteen leagues share one rendering path: eighteen club leagues plus Formula 1, across seven sports. Season history goes back to 2002, and one codebase serves both the web app and the Android build through Capacitor.