Skip to content
Getting word out

Say it once, and it reaches the people it should

BallOBall carries a message down the paths a person actually watches: the in-app inbox with its bell, web push to a phone they have opted in on, email, and the team's own private chat. A coach posts a training change once and it reaches the roster. Match alerts and the day-before reminder send themselves off the fixture list. League-wide campaigns are there too, for admins whose league has the Notifications module switched on.

The group chat is not a distribution list

Today the news lives in a chat thread on somebody's phone, and that thread still contains two players who left last season and none of the parents who joined this one. The venue moves and four people turn up at the old one. The away coach learns the new throw-in time from a screenshot of a screenshot. Nobody can tell who saw it, because in a thread that long there is no difference between read and ignored. And the fixture list, the roster and the results already sit somewhere else — so every message has to be retyped by hand out of the system that already knows the answer.

How it works

Four steps, no configuration

  1. Write it where the roster lives

    A coach or captain posts an announcement to their team and the recipient list is built from the roster, not from a phone's contacts. A league admin whose league has the Notifications module on can instead compose a campaign, pick the audience — everyone, admins, followers, or one team — and see the recipient count before sending.

  2. It fans out to the paths that apply

    The same message lands in each person's in-app inbox, goes out as web push to any device they opted in on, and is emailed where that particular beat sends email. SMS is wired into the campaign composer, but that leg is dormant in production, so plan around the three that are live. Nothing is copied between channels by hand. Roster entries nobody has claimed as a user account are skipped rather than quietly counted as sent.

  3. Each person keeps their own switches

    Every user has a server-stored preference matrix grouped into live match, league news, system and officiating, and a fan following several leagues can mute one of them without unfollowing it. The delivery paths check those preferences before sending, so a toggle switched off genuinely stops the message.

  4. The routine runs without you

    A scheduled job handles the beats nobody should have to remember: the reminder to both teams about a day before a match, and any campaign or drip step parked for a future send time. The job is designed to remind each match once, and a failing campaign is isolated so one bad send does not stop the game-day reminders.

Capabilities

What gets sent, and who controls it — the SMS leg is dormant

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. 1 are built and waiting on setup before they can be switched on.

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

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

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

  • Match-day alerts to league followers (live, score, period end, final)

    Fans who follow a league receive match alerts — match going live, scoring plays, period end and final score — in their inbox and as web push, driven straight off the scoring write path.

    Two opt-in layers apply: the per-league follow toggles (notifyOnLive / notifyOnFinal, both default on at first follow) and the user's global per-type preference. Email is sent for the FINAL milestone only — live, score-change and period-end beats are inbox and push only (backend/src/services/notificationService.ts:365).

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

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

  • Transactional email delivery

    Invitations, receipts, digests, reminders and billing notices are sent through a single transactional email service with a verified sender domain, per-message tagging, and failure handling that keeps a dead mailer from destroying the surrounding action.

    All mail leaves from one sender address (noreply@balloball.com); a tenant's own display name in the From line is available only to tenants entitled to white-label branding (backend/src/services/sesService.ts:47, backend/src/services/tenantEmailBrandService.ts:46). There is no per-tenant sending domain and no retry queue — a recipient-level rejection is logged and reported back as not sent.

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

  • Scheduled campaigns and drip sequences

    Campaigns can be parked for a future send time, and multi-step drip sequences can be built, started and cancelled; a scheduled job runs every five minutes and delivers whatever has come due.

    Same league-admin plus NOTIFICATIONS-module gate as the campaign surface. Sequences are cohort-timed — every recipient receives step N at the same wall-clock moment, not relative to their own signup — and the template and its steps can only be edited while the sequence is DRAFT; once started, edits are refused with a 409 until it is cancelled back to DRAFT (backend/src/handlers/commsSequenceHandler.ts:18).

  • Redacted per-recipient delivery log

    Each campaign records a per-recipient delivery outcome — sent, skipped or failed, with a reason — and the admin drill-down shows those rows with the email address or phone number masked so raw member contact details never leave the backend.

    The log records the outcome of the send attempt only; it does not track opens, clicks, bounces or unsubscribes.

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

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

  • Authentication email (sign-up, verification, password reset)

    Sign-up verification and password-reset email is generated by the platform's own sender rather than the identity provider's default mailer, so the content and branding are under platform control.

    Delivery depends on the mail provider's API key being present in the environment's secret store; if it is missing the auth Lambda fails loudly rather than silently dropping the message.

  • Reliability handling for the scheduled comms job

    The scheduled communications job isolates each of its sweeps so one failing campaign cannot suppress game-day reminders, and any run that fails outright lands on a dead-letter queue with an alarm attached rather than disappearing.

    This is operational plumbing, not a customer-facing uptime commitment. No delivery SLA, retry-until-delivered guarantee, or per-message tracing is offered.

Questions

Before you ask

Does everyone have to install an app to be notified?

No. The bell and the inbox work in any browser session, with no install. Web push is an opt-in layer on top: the user grants browser notification permission from their profile settings, and after that alerts reach the device with the tab closed. Safari delivers web push only to a site installed to the home screen, so iPhone users add BallOBall to their home screen first. Push also needs a VAPID key pair configured for the environment — without one the platform logs it and moves on. Treat push as best-effort: the in-app row is written either way, so a push that does not land is not a message lost.

Can we text people?

Not today, and this is the honest limit of the feature. The SMS channel and the two-way SMS inbox are built and routed in code, but they are dormant in production — the messaging provider's credentials are still placeholders, so nothing is sent or received and the admin inbox renders empty with an explainer. The one capability below that names SMS covers email and in-app today; the SMS half waits on those credentials. Two more campaign extras sit in the same state: AI draft-assist falls back to a deterministic template rather than model-written copy, and per-recipient auto-translate soft-fails to the original text. Plan around email, push and in-app.

Can I see who actually read it?

Partly, and it is worth knowing which part. Team chat and the structured logistics channel track reads. Campaigns record a per-recipient outcome — sent, skipped or failed, with a reason — shown to admins with the email address and phone number masked, so raw member contact details are not surfaced in the admin view. What is not tracked is opens, clicks, bounces and unsubscribes. The log tells you the result of the send attempt, not what the recipient did next.

Will fans be pestered every time somebody scores?

Two switches sit in front of every match alert. A follower's per-league toggles cover going live and the final score, and the user's own per-type preference sits above those. Email goes out for the final only — the live, score-change and period-end beats are inbox and push. Someone following four leagues can mute one of them and keep the other three.

Who is allowed to message a whole league?

League admins, and only where the league has the Notifications feature module switched on — with it off, the whole campaign surface refuses the request rather than half-working. The mass-campaign composer and the club-wide, multi-league version also wait on a connected payment provider, so they activate once billing is set up. Team announcements and team chat are not gated that way: a coach or captain posts to their own team, and a coach keeps edit, pin and delete rights over the history.

Does the weekly report send itself?

Not on its own. The weekly league digest — standings leaders, top performers, recent results, upcoming fixtures — is composed and previewable for you, but an admin has to open the league and press send. The things that do run unattended are the pre-game reminders and any campaign or drip step you scheduled. One more boundary worth stating plainly: the guardian-visibility policy for youth messaging records a breach for an administrator to review, it does not block the message.

Send the next update from where the roster already lives

Public leagues need no account to open.