11 KiB
Push Notifications — Ideas Board (nothing decided)
Status: brainstorm, NOT a spec. The push foundation is built; zero product triggers are wired. This doc is a menu of candidate notification types to pick from later — none of them are commitments, and the list is meant to be argued with, pruned, and added to.
Guiding principle (the one non-negotiable)
Minimal by default. Strictly opt-in. Do not annoy.
- Every notification type ships OFF unless the user opts in — and even opted-in volume stays low.
- Push is a scarce resource: each notification spends trust. One annoying ping and people nuke permission at the OS level and we never get it back.
- Only a tiny high-value default-on set for users who flip the master toggle. Current candidates: "you're up next" and "you were claimed as a partner" — both rare, both personally about you, both actionable right now.
- Prefer notifications that are: about the recipient personally > rare > time-sensitive > tappable to something useful. Anything failing two of those four probably shouldn't exist.
What already exists (the foundation)
src/lib/push/index.ts—sendPushToPlayer(playerId, { title, body, url?, icon?, tag? }). Sends to every device the player subscribed, auto-prunes dead endpoints (404/410). The file itself documents the trigger seams as deliberately unwired.push_subscriptionscollection —pb_migrations/1784200000_created_push_subscriptions.js, service helpers insrc/lib/pocketbase/services/push.ts.- Opt-in settings toggle —
src/features/settings/components/notifications-section.tsx(per-device enable + "send test notification", viasrc/hooks/use-web-push.ts). Today it's one master switch; per-type preferences are a follow-up idea (see bottom). - SSE domain events —
src/lib/events/emitter.tsalready typesmatch | reaction | tournament | team | player | badgeevents, emitted from real product code paths (src/features/matches/server.ts,src/features/tournaments/server.ts,src/features/badges/server.ts,src/features/players/server.ts,src/features/teams/server.ts). Most push triggers can hang off the exact same emit sites — the moment weemitServerEvent(...), we already know something notification-worthy happened.
Again: no trigger below is wired. The point of this board is to choose which few deserve to be.
The menu
Columns: what fires it, who gets it, why it drives engagement, roughly how often it would fire, an annoyance-risk rating, and a suggested default (all of these are suggestions, not decisions). "Default ON" here means on once the user enables notifications at all — nothing fires for users who never flip the master switch.
BEFORE the tournament
| Candidate | Trigger | Audience | Why it hooks | Frequency | Annoyance | Default |
|---|---|---|---|---|---|---|
| New tournament posted / enrollment open | Tournament created or opened for enrollment (src/features/tournaments/server.ts create emit, ~line 27) |
All players | The starting gun — nobody wants to hear about a tournament secondhand | Rare (per tournament) | Low | Off (opt-in "announcements") |
| Enrollment closing soon | Timed job N hours before enrollment lock | Enrolled-nowhere players | FOMO nudge for fence-sitters | Once per tournament | Med — it's marketing, not information | Off |
| You were claimed as a partner | Free-agent partner assignment (assignPartners path in src/features/tournaments/server.ts, emits team + tournament, ~lines 385–397) |
The claimed free agent(s) | Deeply personal — "someone picked YOU." Answers the question free agents are actively anxious about | Once per tournament per free agent | Low | ON (candidate default) |
| Predictions are open | Bracket generated / predictions window opens (submitPrediction lives in src/features/predictions/server.ts) |
Enrolled players | Predictions only work if people fill them out before lock | Once per tournament | Low-med | Off |
| Predictions closing soon | Timed job before first-match lock (predictions lock on first match start) | Enrolled players who haven't submitted | Targeted (only non-submitters), genuinely useful deadline | ≤1 per tournament | Low-med | Off |
| Side bets are open on a match | Match hits ready (book opens per docs/side-bets.md) |
Players with tokens, not in that match | Side-bets spec explicitly wants this nudge for marquee matches | Could be every match — needs throttling (e.g. finals only) | High if per-match; low if finals-only | Off |
| Tournament starts in 1h | Timed job vs. tournament start time | Enrolled players | Logistics — get people in the building | Once per tournament | Low | Off (but a strong opt-in) |
DURING the tournament
| Candidate | Trigger | Audience | Why it hooks | Frequency | Annoyance | Default |
|---|---|---|---|---|---|---|
| You're up next / your match started | Admin starts the match — startMatch in src/features/matches/server.ts (~line 142, emits match) |
The 2–4 players in the match | The single most valuable push in the app: personal, urgent, actionable ("get to the table") | 3–6 per player per tournament | Low — this is the notification people install the PWA for | ON (candidate default) |
| Score reported on your match — confirm needed | reportMatchScore (~line 828) when the other team reports |
The team that must confirm | Unblocks the bracket; the confirmer often doesn't know they're the bottleneck | ≤1 per match played | Low | Off, but arguably deserves default-on — it's a to-do, not a broadcast |
| Your match result is final | finalizeMatch (~line 753, single finalization path for admin endMatch + player confirmMatchScore) |
Players in the match | Closure + "what's next" tap-through | 1 per match played | Low-med (you usually know — you were there) | Off |
| You advanced / you were eliminated | Bracket propagation after finalize (same finalizeMatch seam) |
The advancing/eliminated team | "You're in the semis" is a brag-worthy moment; elimination push is a kindness debate | ≤1 per round | Med — elimination pushes can feel like salt | Off; if built, maybe advance-only |
| Someone reacted to your match | toggleMatchReaction (~line 964, emits reaction) |
Players in the match | Social warm-fuzzies | Potentially very chatty | High — classic notification spam shape; needs heavy batching ("5 reactions on your match") if ever built | Off, probably forever |
| Big upset just happened | finalizeMatch + an upset heuristic (seed gap / prediction consensus) |
Everyone at the tournament | Shared "did you SEE that" moment; drives people to the bracket | 0–2 per tournament if threshold is strict | Med — must be genuinely rare to land | Off |
| Side-bet settled (you won/lost) | Settlement inside finalizeMatch per docs/side-bets.md |
Bettors on that match | Win reveals are dopamine; losses close the loop | Per bet placed (self-inflicted volume) | Low-med — user opted in by betting | Off (or implicit-on for people who bet?) |
AFTER the tournament
| Candidate | Trigger | Audience | Why it hooks | Frequency | Annoyance | Default |
|---|---|---|---|---|---|---|
| Final results / you placed | Tournament completed (tournament status emit in src/features/tournaments/server.ts) |
All enrolled; podium gets a personal variant ("You took 2nd") | The recap moment; personal placement > generic results | Once per tournament | Low | Off (single post-tournament push is defensible as default though) |
| You earned a badge | src/features/badges/server.ts award (~line 44, emits badge with playerId) |
The recipient | Personal, rare, purely positive — near-ideal push shape | Rare | Low | Off, but a strong candidate to promote later |
| Prediction results — how you scored | Final match finalizes → predictions settle | Players who submitted a prediction | Only reaches opt-ins-by-behavior; leaderboard tap-through | Once per tournament | Low | Off |
| Side-bets final standings | Tournament completes → side-bets leaderboard freezes | Players who placed ≥1 bet | Same self-selected audience as above | Once per tournament | Low | Off |
| Your FLXN Replay is ready | (Future feature — no code yet.) Post-tournament recap generation | All enrolled | Personalized recap = the highest-retention artifact we could push | Once per tournament | Low | Off until the feature exists; then a strong default candidate |
AMBIENT / cross-tournament
| Candidate | Trigger | Audience | Why it hooks | Frequency | Annoyance | Default |
|---|---|---|---|---|---|---|
| A rival is in your next match | (Future feature — rivalries don't exist in code yet; head-to-head data does: getMatchesBetweenPlayers / getMatchesBetweenTeams in src/features/matches/server.ts) |
Both sides of the rivalry | "Revenge match" framing is the best narrative hook in the app | Rare | Low-med | Off until rivalries exist |
| New tournament posted | Tournament create emit (also listed under BEFORE — could be the same "announcements" type) | Everyone | Re-engages lapsed players between tournaments | Rare | Low | Off (opt-in "announcements") |
| Free agents need partners | N unclaimed free agents as enrollment deadline nears | Enrolled players without full teams | Matchmaking pressure in both directions | ≤1 per tournament | Med | Off |
| You've been made admin / roster changes touch you | player emit sites in src/features/players/server.ts |
Affected player | Administrative courtesy | Very rare | Low | Off — probably not worth building |
Suggested tiny default-on set (opinion, not decision)
If a user enables notifications at all, they get exactly two things until they say otherwise:
- You're up next —
startMatchseam. - You were claimed as a partner — free-agent assignment seam.
Everything else is opt-in per type. Both defaults are rare, personal, and actionable; neither can fire more than a handful of times per tournament. This matches the seams already name-dropped in the sendPushToPlayer doc comment.
How to wire one (when the picks are made)
The pattern is deliberately boring:
- Find the emit site — e.g.
startMatchinsrc/features/matches/server.tsalready callsemitServerEvent({ type: "match", ... })when the admin starts a match. - Next to that emit, resolve the affected player IDs and call
sendPushToPlayer(playerId, { title, body, url })fromsrc/lib/push/index.ts, withurldeep-linking to the match/tournament route. Fire-and-forget (don't block the server fn on push transport), usetagto collapse repeats of the same kind. - That's it — subscription storage, multi-device fan-out, and dead-endpoint pruning are already handled by the foundation.
Follow-up idea: per-type preferences
Today src/features/settings/components/notifications-section.tsx is a single per-device switch. Once types are chosen, extend that section with category toggles (e.g. "My matches", "Announcements", "Social", "Results & recaps") persisted per player (not per device), and have each trigger check the player's category preference before calling sendPushToPlayer. Categories, not individual types — a settings screen with 15 switches is its own kind of annoying.