One club, every team, every season
A club is not one league. It is a U9 side, three junior teams, two adult teams, sometimes a second section, and the same dozen families threaded through all of it. BallOBall groups those leagues under one club with one member directory, one staff list and one place the fixtures live — so next season starts from this season's structure rather than from a blank spreadsheet.
Organisation
Northgate Sports Trust
Leagues
2
Divisions
6
Teams
48
Players
612
Metro Winter Hoops
Basketball · 4 × Quarter
TeamsW–L
Riverside Youth Football
Soccer · 2 × Half
TeamsW–D–L
What a club year usually looks like
- One family, three children, three of the club's age groups — three separate forms, three payments, and a sibling rate the treasurer works out by hand afterwards.
- Every August last season's U11 squad gets typed in again as this season's U13s, from a spreadsheet somebody emailed round.
- Head of Juniors, Fixtures Secretary and Welfare Officer are real jobs on the committee. In the software they share one login, and when the secretary stands down at the AGM the fixture list goes home with her.
- A player has a birthday in February and moves up to the U15s, but his registration, his subs and his paperwork stay behind with the U13s.
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.
The club, not five separate leagues
Leagues sit inside a club, so the U9s, the U15s and the Saturday adults share one member directory, one staff list and one set of participation, financial and roster reports. Divisions can carry age bounds and the platform blocks an out-of-age player being added to that team, with an explicit admin override when a case genuinely warrants it. Rolling a closed season over carries the same teams and division structure into the next one, with roster carry-forward opt-in.
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.
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.
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.
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.
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.
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.
Club-level participation, financial and roster reports with CSV export
Admins can pull participation, financial and roster reports for a club and export the member directory and each report as CSV.
Restricted to LEAGUE_ADMIN and platform-admin roles.
Sign-up season, and the family that appears in three of them
A parent opens a link, puts more than one child through the same form in a single submission, ticks the waiver and types their name. Confirming a registration puts that player on the roster without anyone retyping them, and a capacity limit runs a waitlist that promotes the next person when a place frees up. The form builder is part of the Registrations module, and the sibling discount does its arithmetic today, but the amount is collected by the club directly until a payment provider is connected.
Online registration form builder
Registrations moduleLeague administrators can build and publish registration forms with custom questions, an optional waiver, fees, capacity limits and an open/close window.
Admin surface is role-gated to SUPER_ADMIN / PLATFORM_SUPPORT / LEAGUE_ADMIN (router.tsx:1006) and the league's REGISTRATIONS feature module must be enabled before the public form will accept anyone (registrationHandler.ts:1411).
Public sign-up page (no account required)
A parent can open a shared registration link and sign a child up without creating an account.
Only works while the form status is PUBLISHED, the league is not archived, the open/close window is current, and the REGISTRATIONS module is enabled — otherwise the page shows a closed/not-found message (registrationHandler.ts:1409-1427).
Waivers with typed e-signature and versioned snapshot
A form can require a waiver; the registrant ticks acceptance and types a signer name, and the exact waiver text and its version are stored with the registration and can be viewed later.
Module-gated with the rest of registration. Signature capture is a typed name plus a checkbox, not a drawn signature or a third-party e-signature service.
Automatic roster enrollment from confirmed registrations
Confirming a registration creates the corresponding player on the roster, and the operation is idempotent so an already-enrolled registrant is not duplicated.
Fires on confirmation, which today is an admin action on the offline path; module-gated with the rest of registration.
Registration waitlist with automatic promotion
Capacity-limited registrations and camps support a waitlist, and freeing a seat promotes the next person waiting.
Only applies to forms and camps that have a capacity set; an uncapped form never triggers the capacity gate.
Parent portal — 'My Registrations' across leagues
A signed-in guardian can see their own registrations across every league in one place.
Requires an authenticated account; anonymous registrants who never signed up for a login do not get this view.
Sibling / multi-child discounts in one submission
Needs a connected payment providerA parent can register several children in a single submission and a configurable sibling discount is applied to each additional child, with the price summary shown before submitting.
Requires the REGISTRATIONS module; the discount percentage is set per form by an admin. The discount arithmetic runs whether or not online payment is live, but the resulting amount can only be collected offline while Stripe is dormant.
The club outlives the coach who leaves in June
Name your own club roles — Head of Juniors, Fixtures Secretary, Welfare Officer — each carrying a chosen set of capabilities from a fixed list, so handing over a job is not handing over somebody's personal login. Admin invitations can be scoped to one league or one sport rather than the whole club. Coaching certificates and safeguarding documents sit against the person with a dashboard of who is expiring soon, though verification is an admin reading the document, not a check with the issuing body.
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.
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.
Teams and coach assignment
League admins can create teams within a league, set team branding, assign and remove coaches, and remove a team.
Team creation is scoped to admins of that league's organization; cross-organization writes are refused. Team short codes must be unique within the league. New teams cannot be added to an archived league.
Player and coach invitations by email plus league join codes
Admins can invite people to a league or team by email, revoke or regenerate a pending invite, and share a short league join code that people can redeem to join; invitation links expire after 14 days.
Invite emails are delivered through a third-party transactional email provider configured per environment; the invite row is still created if the email fails to send. Join codes can be rotated or set to a custom value by an admin.
Player profiles and roster management
Coaches and league admins can add, edit and remove players on a team roster, set jersey numbers, positions and a captain, with positions validated against the league's sport.
Roster writes require coach-of-team, league-admin, or a club role grant carrying LEAGUE_MANAGE_ROSTER. Rosters lock ahead of upcoming matches. Jersey numbers are unique per team. Archived leagues are read-only.
Bulk roster import by paste or CSV
Admins can paste rows or upload a CSV of players, preview the parsed result with per-row errors before committing, and see a per-row success/failure report with a retry for the failures.
Parsing and validation happen in the browser and rows are submitted one HTTP request at a time — there is no server-side batch endpoint, so a large import is a sequence of individual creates.
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.
One hall, six teams, every Tuesday
Generate a round-robin per age group, split the hall into named courts, and get a warning when two fixtures land in the same venue at the same time. Practices go in with going, maybe and not-going replies, one session at a time; there is no repeat rule, so a weekly practice is entered per week. Parents subscribe the fixture list into their own calendar, and whoever is on the table opens a queue of the day's games and taps into the console for that sport.
Automatic round-robin fixture generation
Generate a full round-robin schedule for a league from its team list — the app pairs every team using the circle method, inserts a bye when the team count is odd, and writes the fixtures and matches in a single transaction.
League-admin action only (LEAGUE_ADMIN / SUPER_ADMIN / PLATFORM_SUPPORT, or a club role holding the LEAGUE_MANAGE_SCHEDULE capability — backend/src/handlers/standingsHandler.ts:310). Requires an active season (backend/src/services/scheduleService.ts:227) and at least two non-deleted teams (backend/src/services/scheduleService.ts:214). An archived league is read-only, so a closed season's schedule cannot be regenerated.
Named courts / playing surfaces per venue
Split a venue into named courts or surfaces, and add, rename, delete or reorder them as the building changes.
Court names are a venue-configuration surface; assigning a court to a match is a manual choice in the match dialog, not an automatic allocation.
Venue double-booking detection
Scheduling a match into a window when the same venue is already hosting one raises a conflict that must be acknowledged before it saves.
The server-side check compares the venue only — it does not consider which court within the venue, so two matches on different courts of the same venue are reported as a venue conflict. Court-level precision exists only as an advisory banner in the browser's schedule manager (frontend/src/pages/app/schedulerConflicts.ts:78), which never blocks a save.
Practice scheduling with RSVPs
Schedule team practices with a start time, duration and venue, and collect going / maybe / not-going responses from the squad.
Practices are created one at a time — the API has no recurrence or repeat rule, so a weekly practice must be entered per session.
Calendar subscription feeds (iCal)
Subscribe to a league, team, venue or single match as a calendar feed and open it in Apple Calendar, Outlook or Thunderbird.
A private league's feed inherits the league's access gate. Each league feed returns a sliding window of matches around today, and is capped at a maximum number of events per feed (backend/src/handlers/calendarFeedHandler.ts:283).
Scorekeeper game queue
Scorekeepers get a dashboard listing the league's games in priority order — live first, then today, then the rest of the week — with one-tap entry into the console.
The file states there is no per-match scorekeeper assignment system today; every match in the league is visible to every scorekeeper (frontend/src/pages/app/ScorekeeperDashboardPage.tsx:5).
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.
Say it once, to the right part of the club
A coach posts to their own team and it lands in the in-app inbox and goes out by push and email. Pre-game reminders go out about a day before a match on a scheduled job, without an admin pressing anything. The club-wide send across several leagues at once is badged as needing setup, and every league in that batch must have the Notifications module switched on; the guardian-visibility check on team chat records a breach for an administrator to review rather than blocking the message.
Team announcements
A coach or team captain can post an announcement to their team; it appears on the coach and player dashboards and fans out to each member's in-app inbox, web push, and email.
Editing, pinning and deleting an announcement are coach-only — a captain can post but not modify history (backend/src/handlers/teamFeedHandler.ts:1). Recipients must have a claimed user account; unclaimed roster entries are skipped.
Team chat
Every team has a private chat that any team member — player, coach or captain — can post to, with read tracking, and non-members are refused.
Updates reach other devices by polling on a short interval plus a same-origin cross-tab channel (frontend/src/pages/app/TeamChatPage.tsx:8, :55), not a persistent WebSocket. Chat history is served as the most recent messages, not an unbounded archive. There are no attachments, reactions, edits or deletes — a message is plain text capped at 2000 characters, and no route exists to change or remove one once posted (backend/src/handlers/teamFeedHandler.ts:193).
Pre-game reminders (~24 hours before a match)
Roughly a day before a scheduled match, every member of both teams is automatically reminded in-app, by web push and by email, on a scheduled job that runs continuously without an admin having to press anything.
Recipients are team members with claimed accounts, not league followers or ticket holders. Each match is reminded once, deduplicated. The sweep processes at most 200 matches per run (backend/src/services/pregameReminderService.ts:15) and honours the user's gameReminders toggle, so anyone who has switched that off receives nothing.
In-app notification centre (bell + inbox)
Every user gets an in-app notification inbox with a bell in the top bar, a full notifications page, and mark-one / mark-all-read controls.
The bell is a client-side poll of GET /notifications (routes/index.ts:2096 documents it as the bell-icon poll), not a server-pushed stream. The list returns the user's most recent notifications, not unlimited history.
Club-wide segmented campaigns across multiple leagues
Needs the Notifications module and live Twilio credentialsA club admin can compose one message across a selected set of the club's leagues, segmented by role, and send it as a single batch.
Segments are limited to three fixed role audiences — everyone, admins, followers — with no per-team targeting at club level (backend/src/services/clubCommsService.ts:36). Every league in the batch must have the NOTIFICATIONS module enabled or the whole compose is refused; there are no partial batches with silently skipped legs (backend/src/services/clubCommsService.ts:198).
Guardian-visibility messaging policy (youth safeguarding)
Guardians can be linked to a minor player, and messages that breach the guardian-visibility policy are detected on the chat write path and recorded for an administrator to review and resolve.
IT LOGS, IT DOES NOT BLOCK. The service explicitly does not prevent the message from being sent in this version (backend/src/services/guardianMessagingService.ts:18), and no notification is sent to the guardian when a violation fires. Do not describe this as enforcement: policyAllowsMessage always returns allowed:true today, so the message is persisted and delivered either way and an administrator reviews the violation afterwards.
What members and parents actually see
Set the club's logo, colours and font once and push them onto every league, so the U11 page and the adult page look like the same club — those cosmetic fields need a Pro or Enterprise plan, or the Custom Branding add-on. Each league gets a public page an admin turns on deliberately, with room for an About or Sponsors page of your own. A youth club can hide player names from those pages with one setting, or keep a league off the public directory entirely and let only its own signed-in followers read it.
Organization-wide branding applied across leagues
Pro planAn 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.
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.
League branding on the fan page (logo, header image, colours, font)
A league's logo, header image, brand colours and web font are applied to its public fan page.
Branding falls back through club and tenant defaults; a league that has not set colours renders the platform defaults.
Multi-page league website (custom About / Sponsors pages)
A league can add its own extra pages to its fan site and fans read them anonymously at /l/<slug>/p/<page>.
Only pages the admin has explicitly published are returned; unpublished pages 404. Same public-league resolution as the rest of the fan surface.
Roster-name privacy control for youth leagues
A league admin can hide player names from the public fan pages with a single setting, and the public API then omits them.
A per-league toggle; it is not a per-player consent flow and it does not retroactively remove names from anything already cached by a third party.
Followers can read an unlisted league
A league can stay off the public directory and still give its own followers full access to scores, standings and match pages via a join code or invitation.
Requires the fan to be signed in and to hold a follow row; anonymous visitors still get a 404 for an unlisted league.
Before you ask
Our Fixtures Secretary is standing down at the AGM. How do we hand the job over?
You reassign the role, not the password. Inside a club you create named roles — Fixtures Secretary, Head of Juniors, Welfare Officer — each carrying a chosen set of capabilities from a fixed list, and you move members in and out of them. Admin invitations can be scoped to a single league or a single sport, so an age-group volunteer sees only their own age group. One screen lists every person in the organisation with every access grant they hold and what it covers. That screen is read-only by design: you read the whole picture there, and grants are made and removed on the surfaces that own them.
One family has children in three of our teams. What do they pay, and how do we take it?
Children on the same registration form go through in a single submission, and a sibling discount set per form comes off each additional child, with the total shown before the parent submits. Children on different forms are separate submissions and separate totals. The collecting is the honest gap: card and PayPal checkout stay dormant until a payment provider is connected to the deployment, so a paid form today reserves the place and tells the family the club will collect directly. Payment plans and refunds are inert for the same reason. Club-level participation and financial reports, and a CSV export of the member directory, do work now.
A player turns 15 in February. Can he move up to the U15s mid-season?
Yes — as two roster edits rather than a transfer record. Take him off the U13 roster, add him to the U15 one. If your divisions carry age bounds, the platform blocks an out-of-age player being added and a league admin can apply an explicit eligibility override when the case warrants it; that check needs a birth date on the player. Rosters lock ahead of upcoming matches, so make the move before the lock rather than on the morning of the game. Matches he has already played stay attached to the team he played them for.
Our netball section and our football section are one club. Is that two accounts?
One. An organisation can run leagues in all 23 supported sports with no per-sport charge, sharing the same club, member directory and staff roles, and each league gets that sport's own scoring console and standings columns. The one step to remember: enable a sport in settings before you create a league in it — league creation is refused for a sport the organisation has not turned on.
Last season's U11s are this season's U13s. What carries over?
Rolling the closed league over creates the next one with the same teams and division structure and no match history, and roster carry-forward is opt-in. Archive the finished season and it becomes read-only, so last year's table cannot be edited by accident. The people are the part that persists: the club member directory and the staff list are not rebuilt each August.
Our Welfare Officer needs the safeguarding paperwork and nothing else. Is that possible?
Mostly. A club role is built from a chosen set of capabilities, and coaching certificates and safeguarding documents sit against the person, with a dashboard of who is expiring soon or already expired. Two limits, plainly. Background-check adapters run in stub mode in this deployment — no request reaches Checkr or NCSI, and statuses advance through a canned sequence — so treat that screen as a record of checks you ran elsewhere. And the most sensitive listings, including minors' identity documents and background-check results, need an organisation-wide grant, because a club-scoped role deliberately does not reach them. Verifying a certificate is an admin reading the uploaded document rather than a check with the issuing body, and uploaded documents are served only through short-lived signed links.