Skip to content
The team side

Run your team without a spreadsheet and a group chat

A coach does not need the fixture generator or the tiebreak rules. A coach needs a roster with the right jersey numbers on it, a way to tell the whole squad that the bus leaves at seven, and something that stops half of them forgetting Saturday. BallOBall gives each team its own roster, its own announcements and its own private chat, inside the league the organisation already set up. One login, one roster, and the same list feeds the scoresheet.

The roster lives in four places and none of them agree

Most teams keep a roster four times over: the league's official list, a spreadsheet somebody owns, a printed sheet in the coach's bag, and the group chat that decides who actually turns up. They stop agreeing by week three. A player changes number and the scoresheet keeps the old one. A late signing gets added to the chat but never to the roster, so no fixture reminder reaches them and the scorekeeper cannot find their name to credit the goal. Somebody who left last season is still reading the team's messages. More discipline does not fix that. One roster that the chat, the reminders and the scorekeeper console all read from does.

How it works

Four steps, no configuration

  1. Get the squad in

    A league admin creates the team and assigns its coach. From there the coach adds players one at a time, or pastes last season's list — a preview shows every parsed row and its errors before anything is written, and failed rows can be retried on their own.

  2. Give each player their details

    The roster guards against two active players in a team wearing the same number, and a player who leaves keeps theirs on old sheets without holding the slot. Positions are checked against that league's sport rather than a generic list, and a team can name a captain plus up to two co-captains. Where a division carries age bounds, an out-of-age player is refused as you add them instead of being discovered at the play-offs.

  3. Tell the team once

    A coach or captain posts an announcement and it lands in every member's in-app inbox, their web push and their email. The back-and-forth goes in the team's private chat, which only players, coaches and captains on that team can read or post to.

  4. Let the reminders run

    Roughly a day before each fixture, both teams are reminded in-app, by push and by email, on a job that runs on its own — nobody presses send. Roster entries nobody has claimed with a real account are skipped, which is the quickest way to see who never accepted their invite.

Capabilities

What a coach actually gets

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.

15 of these are included on every plan.

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

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

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

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

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

  • Team Comms 2.0 — structured logistics posts and an audited comms log

    Teams can be given a structured logistics-only channel with pinned posts and read tracking, plus an admin-readable, CSV-exportable audit log of team communications.

    OFF BY DEFAULT — the MODULE_TEAM_COMMS_V2 feature module ships disabled and must be turned on per league before any of these endpoints answer (infrastructure/lambdas/db-init/platform-features.sql:3864, backend/src/handlers/teamCommsV2Handler.ts:226). The audit log only starts mirroring messages once the module is on, so anything posted before that is never captured, and it is readable by league admins or platform root only (backend/src/services/teamCommsAuditService.ts:36).

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

  • Web push notifications (VAPID)

    Users can opt in to browser and mobile web push from their profile settings; the platform registers the subscription, delivers notifications through the standard Web Push (VAPID) gateways, and prunes subscriptions the browser has revoked.

    Requires the user to grant browser notification permission; Safari only supports it inside an installed PWA (frontend/src/services/pushClient.ts:1). Delivery is best-effort and never blocks the underlying action. Push is dormant on any environment whose VAPID keys are absent or still hold the PASTE_ placeholder — sendPush() no-ops, GET /push/vapid-public-key answers 204, and the SPA hides the enable control (backend/src/services/webPushService.ts:77).

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

  • Per-user notification preferences

    Each user has a server-stored preference matrix — grouped into live-match, league news, system and officiating toggles — and the delivery paths check it before sending.

    Toggles are per notification TYPE, not per channel: there is no separate email-vs-SMS-vs-push control and no quiet hours / do-not-disturb, both deliberately omitted (frontend/src/pages/app/settings/NotificationSettingsPage.tsx:17). Per-league mutes ride the same SPA-owned prefs envelope as leagueMuted:<id> keys, but no send path reads those keys — the per-league opt-outs that actually take effect are the notifyOnLive / notifyOnFinal toggles on a league follow.

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

  • Public league page with visibility control, custom slug and custom join code

    Each league gets a public page at its own URL slug, and admins control whether it is public, can set a custom slug, and can opt in to showing team member names — the fan page is part of the free core module set.

    Admin-only controls; the slug is globally unique across the platform and format- and blocklist-checked.

Questions

Before you ask

Is there a lineup builder or a depth chart?

There is a lineup planner, one per match. The coach of that team sets an ordered starting group and a bench, with notes, and the pre-game checklist on the coach dashboard shows whether it has been filled in. A captain can send a suggested lineup up for the coach to accept or dismiss. It is treated as game-planning data, so only that team's own people can read it back — an opposing coach or an anonymous visitor gets a 404. Two things are not there: a depth chart, and a substitution rota to write out in advance. Availability sits on the roster instead — mark a player out or injured with an expected return date and the coach dashboard carries it.

Can a coach manage the roster without being a league admin?

Yes. Roster writes are open to the coach of that team, to league admins, and to anyone holding a club role grant that carries the roster-management permission — so a team manager can be given roster rights without being handed the league. Two limits are worth knowing: rosters lock ahead of upcoming matches, and an archived league is read-only.

Our roster is already in a spreadsheet. Do we retype it?

No. Paste the rows or upload a CSV, and you get a preview with per-row errors before anything is committed, then a per-row success and failure report with a retry for the failures. Parsing happens in your browser and each player is submitted as its own request, so a long list is a sequence of individual creates rather than one bulk upload.

Is team chat private from the league office?

The chat itself is restricted to team members — a league admin who is not on the team is refused by the API. Two things to say plainly before you promise a squad total privacy. A league can switch on Team Comms 2.0, which ships off by default, and that adds an admin-readable, CSV-exportable log of team communications. Separately, where guardians are linked to a minor player, messages that breach the guardian-visibility policy are recorded for an administrator to review — that policy logs, it does not block, and nothing is prevented from being sent.

Does the chat update live?

It refreshes on a short polling interval, plus a same-origin channel that keeps your other open tabs in step. It is not a persistent socket connection, and the history you see is the most recent messages rather than an unbounded archive. Announcements are the channel to use when something must reach people who are not looking at the app — those fan out to the inbox, web push and email.

Half my squad never accepted the invite. What are they missing?

Nothing on the pitch. A roster entry stands on its own, so an unclaimed player still holds a jersey number and can still be credited for events during the match. What they miss is everything that needs a login: pre-game reminders, announcements and team chat all go to people with accounts, so an unclaimed row is quietly skipped. An invite link lasts 14 days. A pending one can be resent, which issues a fresh link and restarts the clock; one already expired or revoked has to go out as a new invite. A coach can send these for their own team without going through a league admin, and coach-sent invites are fixed to the player role.

Give your coaches the team, not the spreadsheet

Public leagues need no account to open.