Skip to content
The table

The table sorts itself, by your sport's own tiebreakers

Soccer ranks on three points a win and goal difference. Rugby adds bonus points. Cricket falls through to net run rate. Basketball goes to win percentage, then head-to-head. Each of the 23 sports declares its own standings columns and its own comparator in code, so the table that comes out is already the one your competition rules describe — nobody configures a ranking formula, and nobody has to defend it in the car park afterwards.

Two teams finish level, and the season ends in an argument

The tiebreaker lives in someone's memory rather than in the table, so when it matters there is nothing to point at. The spreadsheet holding the standings has one owner, one formula and one person who understands the formula, and it is out of date from Sunday night until that person next opens their laptop. Player numbers are usually worse off than the table: they sit on scoresheets in a folder, so "who is top scorer" is a question nobody can answer without an evening of counting. None of that is a maths problem. It is a problem of the results sitting somewhere the table cannot reach.

How it works

Four steps, no configuration

  1. Close the match

    A result counts towards the table only once its match is final. A game still in progress does not move anybody up, so a half-scored fixture does not flatter a team overnight.

  2. The table rebuilds itself

    Standings are computed from the finalised results when the page asks for them, using that sport's own columns and comparator — goal difference, bonus points, net run rate, sets, whichever is correct for the sport the league is playing.

  3. Slice it the way you need it

    Filter to overall, home or away form, scope the table to one division or tournament pool, or to a single season. Clinched and eliminated markers and a magic number come with it, alongside strength of schedule and recent form.

  4. Go deeper when the table is not enough

    Player leaderboards, the stat explorer, side-by-side compare and player game logs are the Leaderboards module; the derived metrics are the Advanced Stats module. Both are switched on per league, and the base standings table needs neither.

Capabilities

The table, the box score, and everything underneath them

Everything below is built and running today. Unbadged items are included on every plan; a badge means the capability needs a module added to your plan, a higher plan tier, or a piece of setup finished first.

8 of these are included on every plan. 2 groups need the Leaderboards / Advanced Stats modules.

  • Sport-specific standings columns and tiebreak chains

    Every supported sport declares its own standings comparator and column set in code, so soccer ranks on a 3-1-0 points table, rugby on 4-2-0 with bonus points, cricket on net run rate, and basketball on win percentage with head-to-head — with no per-sport branching in the shared standings engine.

    The tiebreak order is defined in each sport's plugin source, not configurable by a league admin — there is no UI or league-rules field for reordering tiebreakers. Head-to-head is consumed only by basketball's comparator; the other 22 comparators fall through to their own primary key and then point differential and points for, with cricket inserting net run rate and archery a 10s/Xs countback.

  • Automatic league standings tables

    Standings are computed automatically from finalized match results and shown both in the admin app and on a league's public fan page.

    Only matches with status FINAL are counted (backend/src/services/standingsService.ts:176). The table is recomputed when the page requests it, not pushed to the browser. The STANDINGS module can be toggled off per league, in which case the endpoint returns an empty list rather than an error (backend/src/handlers/standingsHandler.ts:100).

  • Per-sport box scores for every match

    Every match has a box score built by its sport's own projection of the recorded scoring events, and each of the supported sports ships its own box-score table layout in the app.

    Anonymous access is allowed only when the league is public and ACTIVE or COMPLETED, when the match has streaming enabled, or when the viewer follows the league (backend/src/handlers/matchHandler.ts:277-318). Per-player minutes are derived, not directly recorded: they are reconciled from SUBSTITUTION events plus clock progression, only for sports with a real countdown clock, and a side whose opening lineup cannot be established keeps zero minutes flagged minutesReliable false.

  • Home/away splits, division (pool) scoping and season scoping on standings

    Standings can be filtered to overall, home or away records, scoped to a single division or tournament pool, and scoped to a chosen season.

    Pool scoping counts only games where both teams are in that pool. Season scoping requires the league to have Season rows; with no season selected the table is all-time for that league.

  • Playoff race markers — clinched, eliminated and magic number

    The standings table marks teams that have mathematically clinched or been eliminated from the playoff cut, and shows a magic number for teams still alive, computed against each sport's own ranking rule.

    The playoff cutoff is fixed platform-wide at the top half of the group (ceil(n/2)) — backend/src/services/standingsService.ts:508 states there is no per-league playoff-slots column, so operators cannot configure how many teams advance.

  • Strength of schedule and recent-form indicators

    Each standings row carries a computed strength-of-schedule rating, and the table shows a recent-form streak for each team.

    Strength of schedule is a derived rating from opponents' records within the same league; it is not an external or cross-league rating system.

  • CSV export from stats and standings screens

    Standings, leaderboards, the stat grid, comparisons, box scores, player game logs and advanced-stats tables each have a CSV export button.

    CSV is the only export format — there is no PDF or Excel export anywhere in this domain. Exports are generated in the browser from the rows currently on screen (frontend/src/lib/csv.ts:13); there is no server-side export endpoint and no scheduled or emailed export.

  • Standings and stats tabs on the public fan page

    Leaderboards module

    A league's public fan page can show a standings tab and a stats tab, with the standings columns coming from that league's sport.

    Both tabs are module-gated — standings on STANDINGS, stats on LEADERBOARDS (which defaults off). The league must be marked public and be ACTIVE or COMPLETED, or the viewer must be a signed-in follower via a join code or invite (backend/src/handlers/standingsHandler.ts:34-53); an anonymous reader additionally needs PUBLIC_STATS left on, or both tabs come back empty.

  • Player leaderboards ranked by sport-specific metrics

    Leaderboards module

    Leagues can publish leaderboards that rank players by the metrics their sport defines — points and rebounds for basketball, runs and wickets for cricket, goals and assists for soccer.

    The LEADERBOARDS feature module ships DISABLED by default (infrastructure/lambdas/db-init/platform-features.sql:1355, :1697) — a league admin must turn it on before any player numbers are served; until then the endpoint returns an empty list. A single leaderboard ranks by ONE metric at a time, capped at 100 rows and defaulting to 25; an omitted or unsupported metric falls back to that sport's first published metric.

  • Stat Explorer — sortable, filterable season-stats grid

    Leaderboards module

    A season-stats grid shows every player against every metric their sport publishes, sortable by any column and filterable by team, division, games played and player name.

    The page lives inside the signed-in per-sport section (/s/:slug/explorer) — there is no public fan-page route for it. Metric values and columns are withheld unless the LEADERBOARDS module is on (backend/src/handlers/standingsHandler.ts:180-190). The grid reads at most 500 players in one request (backend/src/services/leaderboardService.ts:384) and sorts, filters and exports in the browser; there is no pagination and no server-side query.

  • Side-by-side compare for players or teams

    Leaderboards module

    Two to four players, or two to four teams, can be compared side by side on their season stat lines, with home/away split selection.

    Hard-limited to between 2 and 4 entities per comparison (backend/src/handlers/standingsHandler.ts:219-220). Signed-in per-sport section only (/s/:slug/compare); no public fan route. Numbers are withheld when the LEADERBOARDS module is off.

  • Player game log and season stat line on public player pages

    Leaderboards module

    A player page shows that player's season stat line and a per-game log with opponent, result and sport-specific per-game stats, with overall/home/away splits.

    Returns an empty stat line unless the league has the LEADERBOARDS module enabled (backend/src/handlers/playerStatsHandler.ts:317-319). Minutes in the game log are an explicit estimate derived from which periods a player recorded an event in, flagged minutesEstimated on every row until SUBSTITUTION reconciliation lands, and rendered with a leading ≈.

  • Basketball advanced stats — TENDEX, plus/minus, lineup efficiency, true shooting, usage, off/def rating

    Advanced Stats module

    For basketball, the platform derives advanced metrics — TENDEX, plus/minus, lineup efficiency, true-shooting and effective field-goal percentage, usage and offensive/defensive rating — on demand from the recorded event stream.

    Basketball is the only sport with this layer: supportsAdvancedStats is true only in backend/src/sports/basketball/index.ts:119 and every other plugin sets it false, in which case the endpoints return typed-empty results. The MODULE_ADVANCED_STATS feature module also ships disabled and is seeded as a PRO-plan module (infrastructure/lambdas/db-init/platform-features.sql:2660); with it off the endpoints return an empty shape rather than an error.

  • Sport-native advanced stats for non-basketball sports

    Advanced Stats module

    Most sports declare their own advanced-metric definitions, which the platform derives from recorded events and shows on the match page and in league leaderboards.

    20 of the 23 sport plugins declare the generic advancedStats capability; curling and pool declare none, and basketball uses its separate basketball-only path. Gated behind MODULE_ADVANCED_STATS, which is disabled by default per league.

  • Pitching statistics (ERA, WHIP, K/9, BB/9, K-BB)

    Leaderboards module

    For baseball and softball, pitching lines are derived from the scored event stream, attributing each outcome to the pitcher on the mound.

    Only baseball and softball declare a pitching capability; every other sport returns an empty shape (backend/src/services/pitchingStatsService.ts:18-20). Gated on the LEADERBOARDS module, which defaults off (backend/src/handlers/pitchingStatsHandler.ts:82).

  • Cross-league public leaderboards (state / national scopes)

    A public leaderboards page ranks players across leagues by state or nationally, drawing only on leagues whose operators have opted in.

    A league appears only when League.optInPublicLeaderboards is set true by its admin (backend/src/services/publicLeaderboardsService.ts:305). Snapshot generation is an admin-triggered POST (backend/src/routes/index.ts:1201) restricted to SUPER_ADMIN or PLATFORM_SUPPORT, with no scheduled job in infrastructure/ — the public tables are only as current as the last manual run.

Questions

Before you ask

Can we set our own tiebreakers?

No, and that is worth knowing before you sign. Each sport's tiebreak chain is defined in that sport's plugin source — there is no admin screen for reordering tiebreakers and no league-rules field that overrides them. What you get is the chain the sport is normally played to. If your competition ranks differently, that is a code change on our side rather than a setting on yours.

Are player stats included, or are they extra?

Standings and per-match box scores are included. Player leaderboards, the stat explorer, side-by-side compare and player game logs are the Leaderboards module, and the derived metrics are the Advanced Stats module. Both ship disabled, and a league admin turns them on. Until Leaderboards is on, those endpoints return an empty list rather than half-populated numbers — the absence is deliberate, so nobody reads a partial table as a real one.

How do we get the numbers out of the platform?

CSV, and only CSV. Standings, leaderboards, the stat grid, comparisons, box scores and game logs each have an export button, and the file is built from the rows currently on screen — so filter the table first, then export. There is no PDF or Excel output, and no scheduled or emailed report.

How does it decide who has clinched a playoff spot?

The clinched, eliminated and magic-number markers are computed against the sport's own ranking rule, but the playoff cut itself is fixed platform-wide at the top half of the group. There is no per-league setting for how many teams advance. If your league takes the top four out of ten, read the markers as a race indicator rather than as your competition's own rule.

Does every sport get advanced stats?

Basketball has its own layer — TENDEX, plus/minus, lineup efficiency, true shooting, usage, offensive and defensive rating. 20 of the 23 sports declare a generic advanced-metric set derived from the recorded events; curling and pool declare none. Baseball and softball additionally derive pitching lines such as ERA, WHIP and K/9. All of that sits behind the Advanced Stats module, and the pitching lines behind Leaderboards.

Can fans see the standings without an account?

The standings tab on a public league page renders for an anonymous visitor, provided the league is marked public and is active or completed, or the viewer is a signed-in follower who joined by code or invite. The stats tab on that page needs the Leaderboards module. The stat explorer and side-by-side compare live inside the signed-in section only — there is no public route for either.

Put the table somewhere everyone can see it

Public leagues need no account to open.