Skip to content
For schools

One calendar for every squad in the building

A school year runs three seasons deep, with more squads than there are gyms to put them in. BallOBall holds all of it on one calendar, hands the book to whoever is at the table on game night, and posts the result to a page the school community opens without an account. Student names can be kept off that page with one setting.

Today

What the athletic office runs on today

A women’s volleyball match on an indoor court, a player rising to attack.
  • The master calendar is a whiteboard in the athletic office, and the copy that went home in September has been wrong since the first rain-out.
  • Two squads and a PE class all hold the main gym at four, and nobody finds out until the bus is already idling outside.
  • A parent texts a coach at five to ask what time the bus gets back and who won, because there is nowhere to look either one up.
  • Physicals, consent forms and who is actually cleared to play sit in a filing cabinet in the school office.
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.

Put every squad on one calendar

One organisation covers every sport you field, and a league carries season after season — autumn, winter, spring — rolling forward into the next with the same teams. Fixtures generate themselves from the squad list, and a save is blocked when a squad is already playing in that window. A venue clash has to be acknowledged before it goes through, and any fixture can still be moved, postponed or cancelled by hand.

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

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

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

  • Team double-booking detection

    If you try to schedule or move a match into a window where one of the two teams is already playing, the app blocks the save and names the clashing fixture.

    It is a soft block: an authorized league admin can force the booking through by re-sending with an override flag (backend/src/handlers/matchHandler.ts:1059). Bulk generation never blocks — it returns the overlaps as warnings instead (backend/src/services/scheduleConflictService.ts:288).

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

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

  • Manual / custom fixture creation, reschedule, postpone and cancel

    Create one-off matches by hand with a date, time, venue and court number, then move, postpone or cancel any match from the schedule manager.

    Admin surface — the schedule manager route is restricted to SUPER_ADMIN, PLATFORM_SUPPORT and LEAGUE_ADMIN (frontend/src/app/router/router.tsx:812); a custom club role holding LEAGUE_MANAGE_SCHEDULE can move matches via the API. Cancel is a status change, not a delete — the row stays on the schedule and can be moved back to SCHEDULED or POSTPONED (backend/src/handlers/matchHandler.ts:1302). Deleting it is a separate action.

Hand the book to whoever is at the table

A student manager, an assistant coach or a parent volunteer opens a game queue that puts tonight's fixtures first, then taps what happened in that sport's own console. Scoring survives the gym Wi-Fi dropping — taps land in an on-device queue and send themselves in order when the signal returns. Any entry can be undone, the result locks when the game ends, and the full-screen scoreboard drags onto the gym TV over HDMI.

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

  • Offline-tolerant scoring with a durable queue

    If the connection drops mid-game the scorekeeper keeps tapping: events are written to a durable on-device queue that survives closing the tab, and they are sent in order automatically when connectivity returns.

    The queue protects events captured after the console has loaded. The service worker caches only public read endpoints, never authenticated data (frontend/src/pwa/runtimeCaching.ts:7), so a console cannot be opened cold with no connectivity. Deterministic failures — a 4xx — mark the queued row errored and surface it to the operator instead of retrying forever, and a write that cannot reach IndexedDB at all rejects rather than silently dropping the event.

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

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

  • Full-screen scoreboard for a gym TV / second monitor

    A full-screen scoreboard page opens in one click from the scorekeeper console, drags onto a second display over HDMI, and lets the operator show, hide, resize and reposition every element, with one-click Standard / Minimal / Detailed presets.

    Layout choices are saved per match in that browser's localStorage, not on the server, so they do not follow the operator to another device. For a non-public league the page needs either a signed-in operator or the match's streaming toggle on, because the underlying match read answers 404 to an anonymous caller otherwise; the board then paints an access notice instead of a black screen, and keeps polling so flipping either switch brings it up without touching OBS.

One page the whole school can open

The league page carries the schedule, the table, the scores and your own announcements, and it reads with no account and no app download. Parents subscribe the fixtures into Apple Calendar, Google Calendar or Outlook, so a reschedule moves in their phone rather than in a text you have to send. A school that would rather not appear in the public directory can leave the league unlisted and hand out a join code instead — that route needs the reader signed in.

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

  • No account and no app download needed to view a fan page

    Fans can open a public league page, its standings, schedule, teams, players and individual match pages in any web browser without creating an account or installing anything.

    Following a league/team/player, the personalised feed, notifications, loyalty and ticket wallet all require a signed-in account.

  • Public schedule / fixtures

    A published league's fixture list and upcoming games are readable on its public fan page without signing in.

    Gated on the league's SCHEDULING module (FREE tier, default on) plus the PUBLIC_STATS anonymous-read gate.

  • Public standings table

    Anonymous fans can read a published league's standings table on its fan page.

    Gated on the league's STANDINGS module. STANDINGS is a FREE-tier module that is enabled by default (backend/prisma/seed.ts:88), but a league admin can turn it off, in which case the tab disappears and the anonymous read returns an empty list.

  • Calendar subscription for schedules (iCal / webcal)

    Fans can subscribe a league, team, venue or single match schedule into Apple Calendar, Google Calendar or Outlook, and reschedules flow through to their calendar.

    Public feeds honour the same public-league gate; private league feeds return 401 in the fan's calendar client. Each feed returns a sliding window of the last 90 days plus the next 365 days, capped at 1000 events.

  • League news and announcements on the fan page

    Leagues can post news and announcements that appear on their public fan page.

    Gated on the FAN_NEWS module (FREE tier, defaultEnabled true); a league admin can switch it off.

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

Retire the phone tree

Both squads are reminded about a day before a match by a job that runs on its own, with nobody in the office pressing anything. Coaches post to their team, practices collect going and not-going replies, and followers can be told when a game goes live and when it finishes. Two honest caveats: the weekly digest is composed for you but an admin has to press send, and the league-wide campaign composer is badged setup-required — its SMS channel is not live in production.

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

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

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

  • Match-start and final-score notifications for followers

    Fans who follow a league can opt in to be notified when its matches go live and when they finish.

    Requires an account and a follow row. Per-league opt-ins (notifyOnLive / notifyOnFinal) default ON when a fan subscribes, and the fan can turn each off. Delivery is fan-out on match status change; the in-app bell row is the canonical delivery, while web push and email are best-effort side channels — push is silently skipped without VAPID keys or a browser subscription, and email is sent for FINAL only.

  • Weekly league report digest

    A league admin can preview a composed weekly digest — standings leaders, top performers, recent results and upcoming fixtures — and send it by email to opted-in admins and followers.

    MANUALLY TRIGGERED. There is no scheduled job that sends it automatically; an admin must open the league and press send (backend/src/handlers/leagueHandler.ts:2951, and the only comms cron — backend/src/handlers/commsTick.ts — runs campaigns, pre-game reminders and the recycle-bin purge, never the digest).

  • League mass-communication campaigns (email, SMS, in-app)

    Needs live Twilio credentials

    A league admin can compose one message to a chosen audience — everyone, admins, followers, or a specific team — pick email and in-app channels, preview the recipient count before sending, and see per-campaign sent / skipped / failed counts afterwards.

    League-admin (or platform-root) only, and gated on the league's NOTIFICATIONS feature module, which 403s the whole surface when switched off (backend/src/handlers/messagingHandler.ts:129). The SMS channel is DORMANT in production — the Twilio secret still holds placeholder values, so an admin can pick SMS but every SMS recipient is recorded as skipped with reason not-configured rather than texted (backend/src/services/smsService.ts:29).

Clear a student before they play

Age bounds live on the division, and an out-of-age player is refused both when an admin adds them to a roster and when someone registers — with an explicit override an admin can record when the exception is real. Coach certifications are tracked with expiry alerts, and a graduated return-to-play protocol can be switched on per league. Registration forms, their waivers and the public sign-up link are part of the paid Registrations module; a roster you already hold in a spreadsheet can be pasted or uploaded as CSV without it.

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

  • Online registration form builder

    Registrations module

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

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

Keep student details where they belong

One setting hides player names from the public pages and the public API stops returning them. Uploaded documents are served only through short-lived signed links, roles are scoped down to a single sport, league or team, and a minor's record can be exported or erased by the player, a verified guardian or an admin. Be clear on one limit: the guardian-visibility check on team messaging records a violation for an admin to review — it does not stop the message being sent. The append-only communications log stays dark until a league turns its Team Comms module on.

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

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

  • Secure document vault for household and member records

    Families and organization admins can store residency, medical, insurance and photo-ID documents against a household: the storage key is generated server-side and bound to the uploader with a signed token, the file is never given a public URL, and retrieval happens only through a short-lived signed link.

    Access is limited to a guardian of that household, an admin of the household's organization, and platform staff. The underlying bucket uses S3-managed encryption with public access blocked; there is no customer-managed key option.

  • Guardian links and minor-messaging policy

    Guardians can be linked to and verified against a minor player, team messaging is checked against that policy before a message is stored, and admins get a review queue of policy violations they can resolve.

    The policy currently LOGS violations rather than blocking them — the service's stated v1 contract always returns allowed, so no message is actually refused, and no notification is sent to the guardian when a violation fires. Hard-blocking is described in the code as v2 work behind a per-tenant flag that does not exist yet.

  • Append-only team communications log for youth-safety evidence

    When the Team Comms module is enabled, every team chat message, announcement and logistics post is mirrored to a separate log that the service exposes only insert and read operations for, viewable and CSV-exportable by a league admin.

    Gated behind the MODULE_TEAM_COMMS_V2 feature module, which ships disabled by default and must be turned on per league. Immutability is enforced by the service layer not exposing update or delete methods — it is not a database-level or cryptographic guarantee.

  • Data-subject rights: account deletion, data export and erasure

    A user can delete their own account from within the app, and a minor's data can be exported or erased by the player, a verified guardian, an organization admin or platform staff — deliberately not behind any feature toggle.

    Self-deletion is refused for staff accounts and for anyone who still owns an organization; ownership must be transferred first. Whole-organization export and user anonymization are platform-staff actions, not self-serve.

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

Questions

Before you ask

Can we keep student names off the public pages?

Yes. A league admin flips one setting and the public API stops returning player names, so team pages render without them. It is a league-wide switch rather than a per-player consent flow, and it cannot reach back to remove a name from something a third party has already cached. A school that wants no public page at all can leave the league unlisted and give its own community a join code instead; readers on that route have to be signed in.

Does BallOBall satisfy our district's student-data requirements?

BallOBall holds no compliance certification of any kind, and this page will not pretend otherwise. What we can hand your review board is a description of what the code does: each organisation's rows are separated and that separation is enforced in the database rather than only in application logic, uploaded documents are served only through short-lived signed links, roles are scoped to a sport, league or team, a minor's data can be exported or erased by the player, a verified guardian or an admin, and the production database is encrypted at rest with automated backups. Everything runs in a single region, and encryption uses provider-managed keys — there is no bring-your-own-key option. Give that to whoever signs off on your vendors and let them make the call.

Our winter season ends in a play-off. Can BallOBall draw the bracket?

Yes, in the paid Tournaments module. Round-robin fixture generation for the regular season is free; the bracket builder, the pool stage and the tournament-day tools are not, and the public bracket page sits behind a paid module as well. Bracket sizes are exactly 4, 8, 16, 32 or 64, and byes are not filled in for you — with six squads qualifying, you decide which two slots stay empty before you generate.

Do parents need an account or an app?

Not to look. The schedule, the table, live scores and individual match pages all open in a browser for someone who has never signed in, and the fixtures can be subscribed into Apple Calendar, Google Calendar or Outlook so a reschedule moves in their phone without you sending anything. An account is only needed for the parts that are personal to them: following a squad, being notified when a game goes live or final, and a guardian seeing their own registrations.

We field eleven sports across three seasons. Is that eleven accounts?

One. A single organisation can run leagues in any of the 23 supported sports — all 23, with no per-sport charge — each with that sport's own scoring model, standings columns and terminology. Each sport has to be switched on for the organisation from the Sports settings screen before a league of that sport can be created.

What do we get without paying, and what is extra?

The calendar, the scorekeeper consoles, standings and the public league page are included. Registration forms, knockout brackets, player leaderboards and streaming overlays are modules you add on top. Two things to be straight about: activating a paid module from the module store grants a 14-day trial and the response itself reports that online billing is not yet available, and anything that moves money — registration fees, camp fees, ticket sales — stays dormant until a payment provider is connected to the deployment.

Put the whole athletic calendar in one place