Skip to content
For governing bodies

Set the standard once, then run every affiliate to it

A federation is not a bigger league. It is many organisations, each with its own committee, its own members and its own idea of how a table should be sorted. BallOBall keeps their records apart in the database itself, runs all 23 sports off rule engines that live in code rather than in somebody's spreadsheet, and hands out roles narrow enough that a sport secretary sees their sport and nothing else.

Today

What overseeing member organisations usually looks like

Cricket players on the field at a large oval at dusk.
  • Twelve affiliates, twelve systems. One sends its fixture list as a PDF, the next as a screenshot in a group chat, and the sanctioned calendar is assembled by hand.
  • Two member associations rank their tables by different tiebreakers, so a promotion place gets decided by whichever spreadsheet the appeals panel opens first.
  • A member association runs on one shared committee login. A sanctioned result moves the week before promotion is settled, and the federation cannot name the affiliate officer who moved it.
  • The annual participation return reaches the board as one spreadsheet per affiliate, each counting registered players its own way, and the affiliate totals never add up to the national one.
With BallOBall

The same season, without the spreadsheet

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.

Model the federation, not just a league

One organisation sits at the top, and the competitions you sanction hang beneath it. Leagues can run in all 23 sports, with no per-sport charge, once a sport is switched on for the organisation. Clubs group the leagues that belong to a member association, seasons roll over year on year, and conferences and divisions carry the shape your competition regulations already describe. Administrator invitations can be scoped to the whole organisation, to one league, or to one sport, so a discipline secretary sees only their discipline.

  • Multi-sport organization — 23 sports under one roof

    One organization can run leagues in any of the 23 sports the platform supports, each with its own rules and scoring model, and there is no cap on how many sports an organization enables.

    A sport must be explicitly enabled for the organization before leagues of that sport can be created — league creation returns 403 without a TenantSport row (backend/src/handlers/leagueHandler.ts:564). The Settings → Sports screen only adds sports; disabling is not exposed there and the API has no disable or delete endpoint, so a sport stays enabled once turned on.

  • Clubs — grouping leagues, members, staff and custom roles under an organization

    An organization can group its leagues into clubs, each with its own member directory, staff list and custom roles that carry named permissions, and every new league is placed in a club by default.

    Club screens require LEAGUE_ADMIN or platform-admin roles. The organization's Default Club cannot be archived.

  • League creation and multi-league management

    League admins can create and manage any number of leagues inside their organization from a single leagues index, filterable by status and by sport.

    Creating a league requires the LEAGUE_ADMIN role (or platform admin); coaches, players, referees, scorekeepers and fans cannot. League names must be unique within the organization.

  • Season lifecycle (multi-season leagues, activate / end / rollover / archive)

    A league can hold multiple seasons; admins can create, rename, activate and close seasons, archive a finished league to make it read-only, and roll it over into a new league that carries the same teams and division structure with no match history.

    Exactly one season is marked current at a time, enforced in application code rather than by a database constraint. Archived leagues become read-only for teams, rosters, schedule generation and season changes. Roster carry-forward on rollover is opt-in.

  • Conferences and divisions

    Every league can be organised into conferences and divisions with teams assigned to them, edited from a structure tab on the league page, with no paid module required.

    League-admin scope required. Division short names must be unique within a league.

  • Scoped administrator invitations (organization, league, or sport)

    An organization owner can invite additional administrators scoped to the whole organization, to one specific league, or to one or more sports, so a sport-scoped admin sees only that sport's leagues.

    Organization-wide co-owner grants are restricted to the organization owner; the bulk grant surface refuses to mint a tenant-wide league admin.

  • Organization people directory with scoped access grants

    Admins get one screen listing everyone in the organization together with every access grant they hold and what each grant's scope actually covers.

    The screen is read-only by design — the grant and revoke APIs are live, but there is no mutation UI on this page yet, so changes are made through other surfaces.

Write the standard down once

League rules are checked against the sport's own schema at creation, not argued about at the scorer's table. Age bounds block an ineligible player when an admin adds them to a roster and when someone registers. Certifications carry expiry dates, so a lapsed coaching badge surfaces before match day rather than after a complaint. Module toggles are enforced on the server, so an affiliate cannot switch on a surface you have not sanctioned; the concussion protocol is one of those toggles and ships off until a league turns it on.

  • Sport-specific league rules validated per sport

    League rules such as period structure and game length are entered once in a common wizard, translated into the chosen sport's own vocabulary, and validated against that sport's rule schema so an invalid setting is rejected at creation rather than at scoring time.

    Unknown rule keys are stripped rather than rejected. Leagues created without an explicit sport default to basketball.

  • Age-based division eligibility enforcement

    Divisions and pools can carry minimum and maximum age bounds, and the platform blocks adding an out-of-age player to a team in that division — both when an admin adds them to a roster and when someone registers.

    Requires a birth date on the player; league admins and above can apply an explicit eligibility override.

  • Certification and compliance tracking with expiry alerts

    Coaches, referees and volunteers can upload certifications, admins verify or reject them, and a compliance dashboard lists who across the organization is expiring soon or already expired.

    Available to league admins and platform staff. Verification is a human admin decision on an uploaded document — nothing is checked against an issuing body.

  • Concussion return-to-play protocol

    A graduated multi-step return-to-play workflow lets staff file an incident, advance or set back a player through the stages, defer or clear them, and see which players on a team are currently in protocol.

    Gated behind the MODULE_CONCUSSION_PROTOCOL feature module, which is seeded disabled by default and must be enabled per league. A player can always read their own incident history regardless.

  • Per-league feature module toggles

    Admins can turn individual feature modules on or off per league from a module store, and the platform enforces those toggles server-side rather than only hiding the UI.

    Toggling on a paid module requires the organization to hold the matching plan entitlement (Free covers core org/teams, scheduling, live scoring, standings and fan page; Pro and Enterprise bundles unlock the rest). A module can also be unavailable because the league's sport plugin does not list it, because a platform-wide kill switch is off, or because the catalog row is missing — none of which a league admin can fix from the per-league toggle.

Protect the competition record

The result is the thing you will be appealed about. Each of the 23 sports has its own console and its own event catalogue, so a wicket is a wicket and a bonus point is a bonus point. Finalising a match closes the book on the server. A correction is a server-side undo inside one transaction rather than a number retyped over the top, and when two tablets score the same game the platform is designed to catch the conflicting write rather than let one quietly overwrite the other.

  • Sport-aware scorekeeper console for 23 sports

    BallOBall ships a scorekeeper console for every one of its 23 supported sports, and the console for a given match is selected automatically from that match's sport.

    Twenty-two sports render a dedicated sport-pack console; basketball uses the original ScorekeeperPage (frontend/src/sports/basketball/slots.ts:9 deliberately ships no console slot). The route is role-gated to SUPER_ADMIN, PLATFORM_SUPPORT, LEAGUE_ADMIN and SCOREKEEPER — coaches, players and fans cannot open it.

  • Finalized-match lock ("book closed")

    Once a scorekeeper ends the game, the server refuses any further scoring events on that match, so the official result cannot be moved after the book is closed.

    Reopening a finalized match to score again is restricted to platform staff. The End-game dialog warns when locally queued events are still syncing, but finalizing is not blocked on the queue draining.

  • Match-decided lock in set, game and race sports

    In the set, game and race-target sports, score-moving controls switch off once one side has clinched the match, so play cannot be recorded past the deciding set or leg.

    Implemented in the consoles for the set/game/race family only (badminton, pickleball, tennis, table tennis, squash, racquetball, volleyball, darts, pool). Bowling and curling are excluded by design. It is a client-side guard — the server enforces only the FINAL-match lock, so a decided match that has not been ended can still be written through the API.

  • Server-backed undo of a recorded event

    Any recorded event can be undone from the play-by-play log; the server deletes the event and reverses its score and timeout effects in the same database transaction.

    On a match already marked FINAL, undo is refused unless the caller is platform staff (SUPER_ADMIN / PLATFORM_SUPPORT) — a scorekeeper must ask for the match to be reopened. Archived leagues are read-only.

  • Duplicate-proof event replay (idempotency)

    Every scoring tap carries a unique key, so an event re-sent after a dropped connection is matched to the record that already exists rather than being counted a second time, and two browser tabs on the same device are serialised by an origin-wide lock.

    Protection is per logical tap; it does not reconcile two different operators independently scoring the same match.

  • Concurrent-write conflict detection

    When two writes to the same match collide, the server refuses one with a conflict response and the console prompts the operator to refresh and retry rather than silently losing the entry.

    This is collision detection and retry, not conflict resolution. The queue's own documentation states last-write-wins is the backend behaviour and that simultaneous scoring from two devices is deliberately not reconciled (frontend/src/lib/offlineEventQueue.ts:49).

  • 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.

Keep members apart, and staff in their lane

Separation is enforced by PostgreSQL row-level security, not by application code alone: every query runs on a non-owner database role carrying a per-request tenant identifier. Roles are a fixed set with grants that narrow to a single sport, league or team, clubs can define their own named staff roles, and any user can enrol an authenticator app as a second factor. Administrative actions do write audit records — the action, the actor, the target and a timestamp — but know the limit before you rely on it: that console is readable only by BallOBall platform staff, and your own administrators have no audit screen to open.

  • Multi-tenant data isolation enforced in the database (Postgres RLS)

    Every database query runs through a Prisma extension that switches the connection to a non-owner Postgres role and sets a per-request tenant identifier, so PostgreSQL row-level security policies — not just application code — decide which organization's rows a request can see.

    The API stack defaults RLS_ENFORCEMENT to 'strict', but it is a deploy-time context flag, and the Auth stack deliberately runs it 'off' for Cognito triggers. A signed-in user with no organization (a fan) is mapped to the public scope, which runs as the BYPASSRLS role: such a caller has no tenant rows to be confined to, so what limits those reads is each handler's own WHERE clause — the caller's own user id, or an explicit resource id — rather than RLS. The case RLS exists for, an authenticated member of one organization reading another, still requires a tenant and stays fully enforced.

  • Per-organization data isolation and role-scoped access

    Every league, season, team and player row is stamped with the organization that owns it, API writes verify the caller administers that specific league, and the API runs with Postgres row-level security enforcement enabled by default.

    Platform support and super-admin accounts intentionally bypass organization scope for support purposes.

  • Role-based access control with scoped grants

    BallOBall ships a fixed set of roles — platform admin, platform support, league admin, coach, scorekeeper, referee, player, fan and a scan-only gate agent — and league admins can grant additional roles to members, including grants scoped to a single sport, league or team.

    Roles are a fixed enum; organizations cannot define their own top-level roles. Sensitive listings (minors' ID documents, referee 1099 earnings, background-check results) require an organization-wide grant, because narrower club-scoped grants intentionally pass the coarse role check used for navigation gates — those endpoints call a separate tenant-wide guard that rejects a club-, league-, team- or sport-scoped grant.

  • Custom staff roles with capabilities, at club level

    Within a club, admins can create named roles with a chosen set of capabilities and assign members to them, and those capabilities gate club surfaces such as the club reports.

    Custom roles exist only at the club level, not at the organization or league level, and they draw from a fixed capability list rather than arbitrary permissions.

  • Self-serve two-factor authentication (authenticator app / TOTP)

    Any signed-in user can turn on two-factor authentication with a standard authenticator app from their own security settings, and turn it off again.

    TOTP only — no SMS and no hardware-key second factor. The user pool has MFA set to OPTIONAL, so 2FA is opt-in per user unless a platform operator turns on the platform-wide requirement. Enrollment is presented via a copyable secret and otpauth:// URI, not a rendered QR code — no QR library is a dependency, so the secret is typed or pasted into the authenticator app.

  • Audit trail of administrative actions

    Administrative actions across the platform write audit records capturing the action, the actor, the target and a timestamp, browsable and filterable in a paginated console with a per-organization view.

    Both the platform-wide and per-organization audit feeds are readable only by BallOBall platform staff (SUPER_ADMIN / PLATFORM_SUPPORT); there is no audit-log screen a customer's own admin can open. Writes are best-effort and fire after the mutation commits — if the audit insert fails it is logged to CloudWatch and the mutation still stands, so the trail is not a transactional guarantee.

  • Encryption at rest, automated backups and deletion protection

    The production database is encrypted at rest with automated backups and deletion protection enabled, and uploaded files live in a versioned, encrypted bucket with all public access blocked.

    AWS-managed encryption keys (S3-managed for objects, default RDS storage encryption) — there is no bring-your-own-key or customer-managed KMS option. Backup retention is configured at seven days. Everything runs in a single AWS region (us-east-2 by default), so there is no data-residency choice.

Put the federation's name on it

Set the crest, colours, header image and font once on the organisation and push them onto every league in one action; those cosmetic fields need a Pro or Enterprise plan, or the Custom Branding add-on. Custom domains and the white-label fan page sit in the Enterprise bundle and depend on domain provisioning being configured for the deployment. The public league page, the discover directory and the sitemap are part of the free core set.

  • Organization-wide branding applied across leagues

    Pro plan

    An organization can set a logo, header image, colours and font and push that branding onto every one of its leagues in one action.

    Gated: changing logo, colours, font or header image requires a PRO or ENTERPRISE plan or an active Custom Branding add-on; without it the API returns 403.

  • Custom domains and white-label fan pages

    Custom domains (Enterprise) module

    A league admin can claim a hostname, prove ownership with a DNS record, and — once active — have the league's public pages served on that domain with BallOBall branding suppressed.

    Requires the custom_domains module, which sits in the Enterprise bundle only; the API returns a payment-required error otherwise. A soft cap of three domains per league applies. Activation depends on deploy-time provisioning configuration: the CloudFront path needs EDGE_META_ROUTER_ARN and EDGE_BOT_FLAG_ARN, the Amplify path needs AMPLIFY_APP_ID, and with neither set the record stops at DNS-verified for an operator to activate by hand.

  • White-label fan page (BallOBall name removed from the page title)

    Custom domains (Enterprise) module

    Tenants with the white-label entitlement get a fan page whose browser title carries only the league's name.

    Server-computed from the tenant's custom_domains entitlement — a paid add-on, not available by default.

  • Public league fan page (league microsite)

    Every league can publish a branded public page at balloball.com/l/<league-slug> that fans open in a browser.

    Opt-in, not automatic: a league admin must flip the league to public (POST /leagues/{id}/visibility), which auto-enables the free PUBLIC_STATS module. Only leagues in ACTIVE or COMPLETED status are served; DRAFT and soft-deleted leagues 404. An unlisted league also 404s for anonymous visitors and for signed-in non-followers — only a signed-in follower (they redeemed the join code or accepted an invite) can read it.

  • Discover directory of public leagues

    A public /discover page lets anyone browse and search published leagues by name, city and sport.

    Lists only leagues that are ACTIVE, published, and have the PUBLIC_STATS module on; the query returns at most 100 leagues (take: 100) with no pagination. The 'code-gated' filter chip reads a `visibility` field the public API does not return, so that filter yields an empty grid — and so does the 'public' chip for the same reason; only the 'all' chip shows the directory.

  • Search-engine discoverability of league pages (sitemap, per-route meta, social share cards)

    Every published league is listed in the site's generated sitemap.xml and linked from a crawlable /discover index, and each page emits its own title, description, canonical and Open Graph/Twitter card tags.

    Prerendered, not server-rendered. Each published league gets its own /l/<slug>.html at build time, served by one literal Amplify rewrite per published slug (applied out of band by infrastructure/apply-league-rules.mjs, because the league set is live data), so a crawler that does not execute JavaScript reads the title, description, canonical and Open Graph tags of that league rather than those of the homepage. Two limits remain: the page BODY is still client-rendered, so the prerendered shell carries meta only and no fixtures or standings; and a league published after the last build needs BOTH a rebuild (to emit its file) and a rerun of the applier (to add its rule) before crawlers see it, falling through the SPA catch-all until both happen — degraded, not broken. The server-rendered alternative (CloudFront + Lambda@Edge → /meta/render) is still deployed on its own *.cloudfront.net domain with no DNS pointed at it.

Report upward without another round of spreadsheets

Participation, financial and roster reports for a club export as CSV, and every standings table, leaderboard and stat grid on screen has an export button — CSV is the only format, generated from the rows in front of you. Standings merged across your leagues for one sport are part of the paid Leaderboards module, as are public player stat lines. The public cross-league leaderboard is opt-in league by league and rebuilt when an operator asks for it, not on a schedule.

  • Organization and club reporting with CSV export

    Club administrators can pull cross-league participation, financial and roster reports and download each as a CSV.

    Gated on the CLUB_VIEW_REPORTS capability — available to platform staff, the organization's league admins and owner, or a holder of a custom club role carrying that capability. Read-only.

  • 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.

  • Per-sport aggregate standings and leaderboards across an organisation's leagues

    Leaderboards module

    Inside a signed-in organisation's per-sport section, standings are grouped by league and leaderboards are merged across that organisation's leagues for the chosen sport.

    Scoped to one signed-in tenant's own leagues; anonymous callers receive an empty result and there is no cross-tenant public firehose (backend/src/handlers/sportHubHandler.ts:16-19). Each league is still subject to its own STANDINGS / LEADERBOARDS toggle.

  • 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

We sanction twenty member associations. Does each one get its own account?

That is the first decision to make, and it is a genuine trade-off. The organisation is the isolation boundary: every row is stamped with it and row-level security keeps queries inside it. Run your affiliates as clubs inside one federation organisation and you get a single people directory, per-club staff roles and reporting that rolls up across all of them. Give each affiliate its own organisation and the separation is absolute — but there is no parent-of-organisations console that reads across tenants, so the roll-up goes away. Today the only cross-organisation surface is the public leaderboard that leagues opt into one at a time.

How separate is one member's data from another's, really?

Every database query passes through an extension that switches the connection to a non-owner Postgres role and sets a per-request tenant identifier, so PostgreSQL policies decide which organisation's rows a request can see rather than application code alone. Two caveats worth knowing before you sign: the enforcement level is a deploy-time setting on the API, and BallOBall's own platform-support accounts intentionally cross the boundary so they can support you.

Can our own administrators see who changed a published result?

Not yet, and this is the plainest gap on this page. Administrative actions do write audit records — the action, the actor, the target and a timestamp — but the audit console is readable only by BallOBall platform staff. There is no audit screen your own admins can open, and the writes are best-effort after the fact rather than part of the same transaction. What is in your hands today: finalising a match locks it on the server, an undo reverses the score in one transaction rather than overtyping it, and set and race sports lock again the moment the match is mathematically decided.

Do you support single sign-on for our member associations' staff?

No. Sign-in is email and password on Amazon Cognito with email verification required, and any user can add an authenticator app as a second factor from their own settings. Google and Apple buttons are built into the product but are not switched on. There is no SAML or SSO federation, no published API and no webhooks. If your procurement depends on those, BallOBall is not the right fit yet — better to know that now than in month three.

Can we force every affiliate to rank its table the same way?

Yes, and the same design means you cannot override it. Each sport declares its own standings comparator and column set in code — soccer on a 3-1-0 points table, rugby on 4-2-0 with bonus points, cricket on net run rate — so an affiliate cannot invent a sort order of its own. The limitation is the mirror image: the tiebreak chain is not configurable by an admin, so if your competition regulations use a tiebreaker the sport plugin does not, there is no screen to change it.

Can we put our crest and our own domain on the member sites?

The crest, colours, header image and font are set once on the organisation and pushed onto every league in one action; those cosmetic fields require a Pro or Enterprise plan or the Custom Branding add-on. A custom hostname per league is proved with a DNS record and sits in the Enterprise bundle, with a soft cap of three domains per league, and whether it activates depends on the deployment having domain provisioning configured. Treat custom domains as a conversation to have with us rather than a switch to flip.