# 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_subscriptions` collection** — `pb_migrations/1784200000_created_push_subscriptions.js`, service helpers in `src/lib/pocketbase/services/push.ts`.
- **Opt-in settings toggle** — `src/features/settings/components/notifications-section.tsx` (per-device enable + "send test notification", via `src/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.ts` already types `match | reaction | tournament | team | player | badge` events, 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 we `emitServerEvent(...)`, 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:

1. **You're up next** — `startMatch` seam.
2. **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:

1. Find the emit site — e.g. `startMatch` in `src/features/matches/server.ts` already calls `emitServerEvent({ type: "match", ... })` when the admin starts a match.
2. Next to that emit, resolve the affected player IDs and call `sendPushToPlayer(playerId, { title, body, url })` from `src/lib/push/index.ts`, with `url` deep-linking to the match/tournament route. Fire-and-forget (don't block the server fn on push transport), use `tag` to collapse repeats of the same kind.
3. 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.
