Sports Analytics

Rugby Analytics Platform

Context

A rugby analytics platform aggregating stats across 10 competitions, 500+ teams, and 10,000+ players — data that arrives from multiple external sources and has to stay current without manual updates.

The Hard Part

Sports data goes stale fast and users will not wait for it: ingestion runs on 5 scheduled cron jobs across 4 external APIs, and pages still had to load in under a second.

What I Built

  • Data ingestion from 4 external APIs on 5 scheduled cron jobs
  • Stats coverage for 10 competitions, 500+ teams, and 10,000+ players
  • Next.js and TypeScript front end tuned for sub-second loads
  • Supabase as the data layer

The Build

Two requirements that pull against each other

A sports statistics platform has to satisfy two demands that point in opposite directions. The data must be current, because sports data goes stale fast and a standings table showing last week's positions is worse than useless — it is confidently wrong. And the pages must be fast, because nobody waits to look up a score.

The obvious way to be current is to fetch on request: someone opens a team page, the platform asks the source, the answer is as fresh as it can possibly be. That approach is also the reason the page is slow. It puts four external services on the critical path of every page load, each with its own latency, each capable of being slow or unavailable at the exact moment a user arrives — and none of them under your control.

So the problem is not really "how do we get the data" or "how do we make the site fast". It is how to decouple the moment data arrives from the moment a page is served, so that neither requirement is paid for by the other.

Four sources you do not control

Ingestion runs across four external APIs, and the difficulty of aggregation is that four sources are not one source repeated. They differ in schema, in reliability, in rate limits, and in how often they themselves update — so the same underlying fact can arrive in four shapes, on four schedules, with four different notions of what a well-formed response looks like.

Aggregation also creates a problem that neither source has on its own: the same team or player appears in more than one feed, identified differently by each. Reconciling those into one entity is inherent to combining sources, and it is the work that determines whether a player page shows one career or two half-careers.

Scale sharpens all of it. Ten competitions, 500+ teams, and 10,000+ players is a large relational surface, and it is the list and filter pages — standings, squads, comparisons — where that size turns into slow queries rather than the individual record views people usually optimise first.

Then there is the scheduling compromise. Five cron jobs across four APIs means the jobs do not map one-to-one onto sources, which is what happens when different data has different volatility — fixtures, squads, and results simply do not need refreshing at the same rate. A single global schedule is either too slow for the fast-moving data or wasteful for everything else.

Reading and writing on separate paths

The architecture's defining property is that the read path never touches an external API. Cron jobs ingest from the four sources on their own schedules and write into Supabase; every page then serves from Supabase alone. Requests and ingestion share data but never share a moment in time.

The most valuable consequence of that split is what happens when a source misbehaves. If an external API is slow, times out, or goes down entirely, no user experiences it — the site keeps serving the last successfully ingested data at full speed. The outage degrades freshness rather than availability, which is precisely the right trade for statistics, where slightly older numbers are acceptable and an unreachable page is not.

It also changes what performance work means. With no external calls in the request, sub-second loads are not a caching problem layered over a slow dependency — they are a database problem, which is a far more tractable one. The data can be stored already shaped for the queries the pages actually run, because ingestion is where reshaping belongs and it happens on a schedule rather than under a user's cursor.

TypeScript across ingestion and rendering matters more here than in an ordinary application, because the boundary between four external schemas and one internal model is exactly where silent mismatches occur. A field a source renames, or starts returning as a string, is the kind of change that produces plausible-looking wrong numbers rather than an obvious crash — and typed normalisation at the ingestion boundary is what turns that into a failure someone notices.

What the numbers describe

Ten competitions, 500+ teams and 10,000+ players describe the aggregation surface rather than a traffic figure, and that is the honest way to read them. They are a measure of how much heterogeneous external data was reconciled into one coherent model — the part of this kind of platform that consumes the most effort and is least visible in the result.

Sub-second page loads is the figure the architecture exists to produce, and it should be read together with the ingestion design rather than as a separate front-end achievement. A site of this shape is fast because no page load waits on somebody else's API, which is a structural property rather than an optimisation. Tuning a front end cannot rescue a request that is blocked on a third party having a bad day.

What the numbers do not show, and worth stating plainly, is the failure mode this design avoids: the version of the platform where a source going down takes the site with it. That absence never appears as a metric, which is usually true of the decisions that matter most.

Measured Results

10

competitions

500+

teams

10,000+

players

Sub-second

page loads

Tech Stack

Next.jsTypeScriptSupabase
Rugby Analytics Platform — interface screenshot
Visit rugbyreference.com

More Case Studies

Have a similar project?

I would love to help you build something great. Let's discuss your requirements.