Skip to content

01Architecture

3 min read

By Wyatt McPherson

Nineteen leagues, one registry

Scorebug covers hockey, football, basketball, baseball, soccer, cricket and Formula 1. Adding a league is one entry in one file — and four places that entry does not reach.

  • Architecture
  • PostgreSQL
  • Supabase
  • APIs

In short

How do you structure an app that covers many sports leagues without forking the code for each one?

Put every per-league difference in one registry and derive the rest. In Scorebug, colours, upstream API configuration, news sources, surface treatments and display labels are all read from a single league registry through one lookup function, so nineteen leagues share one code path instead of nineteen forks. The discipline that makes it work is simple: anything that cannot be derived from the registry counts as a defect, and gets fixed.

01

The naive version does not survive league four

Sports data looks uniform from a distance and is not. 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 each of those differences is handled where it is noticed, the codebase acquires a switch statement per surface. By the fourth league, adding the fifth means finding every one of them.

02

One registry, one lookup

The alternative is a single registry entry per league that carries everything that differs, and a lookup function that every surface uses to ask about the league it is rendering. Colours, ESPN configuration, whether date ranges are supported, news sources, surface treatments and display labels all come from that one place.

The payoff is that adding a league genuinely is 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

The four places the registry does not reach

It is 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 generation script and its dictionary, because crests are produced ahead of time. Then the news aggregation edge function, which is the one that bites.

That last one is the interesting failure. The aggregator runs on Deno inside Supabase, and it cannot import the browser-side registry. So it carries its own list of leagues. Forget to add the new league there and everything looks correct: the league appears, its scores load, its badges render. Only the news feed is silently empty, and nothing errors, because an empty result is a valid result.

04

Empty results are the hardest failures

A crash tells you where to look. A league that quietly carries no stories looks exactly like a league that had no news today. The difference only shows up if you go looking for it, or if a user tells you. Nobody has come to rely on the news feed yet, so nobody tells you.

The general lesson: when a configuration has to be duplicated across a runtime boundary, the duplication needs an alarm. Either a build-time check that the two lists match, or a runtime assertion that a configured league returns a non-empty source list. A comment saying "remember to also update the edge function" is not a control.

05

What the registry bought

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

None of that is remarkable engineering on its own. It is the ordinary result of pushing every difference into one file early, and then being disciplined about the cases that do not fit. That second half is most of what shipping a multi-tenant surface actually consists of.