# X1 — Notifications — Session Card

> **Module:** X1 Notifications · **Type:** cross-cutting X-module (not one of the 8 protocol engines)
> **Closes:** Divergence #3 (Notification Center working on iOS, entirely missing on Android)
> **Planning artifact only — no production code.** Read alongside `docs/plan/13-execution-modules-and-sessions.md` (the framework this card implements), `03-protocol-engines.md`, `04-backend-togo.md`, `12-legacy-reference-map.md`, `02-platform-divergences.md`, and the relevant platform PRD (05 web, 06 chrome, 07 apple, 08 android, 09 desktop, 11 mcp).

## Purpose

X1 builds the notification stack end-to-end: the backend decides **what** to notify and **when** (server-side, from Protocol Engine hooks and windows — never a client), stamps `notification_items` rows that carry a message key plus a `vars` JSON blob **beside** the sentence (never interpolated server-side, because Arabic reorders), routes each item through a per-user SSE hub that **suppresses push when a live stream is already open**, and — critically — finally wires **actual delivery** that the legacy build never shipped: FCM for Android/Chrome, APNs for Apple, backed by device-token storage. Every client then renders a notification center and handles its platform's push/local-scheduling in a thin, presentation-only way. This is the module that turns "we compute alerts but never deliver them" into working smart alerts across every surface, and it closes Divergence #3 by giving Android the full notification infrastructure it has always lacked.

## Feature parameters relevant to this module (exact — do not invent)

**Notification rules & routing (legacy precedents — PENDING ratification, not spec-fixed; reuse `notify.go` / `internal/notifier/` / `internal/realtime/route.go` as reference, do not port wholesale):**
- In-process sweeper cadence: **15-minute** sweep (legacy `internal/notifier/`).
- Notice rules: caffeine-window-opened alert **20 min**; GERD/shutdown warning **30 min**; weigh-in prompt only **07:00–11:00**; **one notice per kind per user per local calendar day**.
- Routing decision (`internal/realtime/route.go` precedent): **live SSE stream open → suppress push**; else most-recent device within a **12-hour** window → primary device → none — **every decision carries a `Reason`**.
- Content: `notification_items` store the source sentence + `vars` JSON separately; the **client** does ICU rendering. Never interpolate server-side.
- Delivery targets: **FCM** (Android + Chrome), **APNs** (Apple). Legacy registered **no** FCM/APNs tokens — delivery is net-new here.

**Underlying engine constants that generate notifications (owned by the engines, NOT recomputed here — X1 only fans them out):**
- Hydration: **250 ml** units, **5000 ml/day** max, **30-second** cooldown.
- Caffeine Block: no caffeine first **90 minutes** post-wakeup.
- GERD Window: **4-hour** pre-sleep window (water, chamomile, anise only).
- Medication Grace: **60-minute** window.
- Trigger Families: categorical Gout / IBS-GERD / Fatty Liver (Safe vs Trigger-bearing).
- Cycle: 3 consecutive logged cycles before predicting; fails safe on irregularity.
- Kinetic Pomodoro (legacy precedent, PENDING ratification): 25 min work / 5 min short break / 15 min long break / long break every 4 cycles / daily target 8.

**Color / i18n (strict):** alerts render `#FF3333` red **glow** in dark (`#1A1A1A` bg); light-mode alert red is **genuinely undefined — PENDING DESIGN**. All strings ICU Message Format, en/ar, full Arabic RTL (mirrored), **no concatenation**.

## Session table

Surface order within the module (doc 13 §4): **backend → web → mcp → chrome → apple → android → desktop.** Backend must land before any client session. The backend cell is split into two clearly-bounded slices (doc 13 §1: "when a cell is too big, one clearly-bounded slice of it") because delivery is externally blocked on credentials only Fady can supply.

| Session ID | Surface | Scope (this one session builds) | Entry deps | Done criteria (doc 13 §5 template) | Plan docs to read |
|---|---|---|---|---|---|
| `S-<date>-X1-backend-01-NN` | backend | **Notification core.** Notifications plugin/subscriber registered on the kernel hook bus; rules that react to engine hooks (`entry.logged`, engine window transitions) + in-process **15-min sweeper** producing `notification_items` rows (message key + `vars` JSON stored beside the sentence, never interpolated; read-state; engine/kind; user_id). Per-user **SSE hub** (resource-name events, no state → clients refetch) + **push-suppression routing** (live stream open → suppress; else most-recent device 12h → primary → none, each with a `Reason`). Actual push **send** is stubbed here (a routing decision only), landed in backend-02. Endpoints: `GET /api/v1/notifications` (list unread), `PATCH /api/v1/notifications` (mark read). | M0.1–M0.4 (foundation backend); **M1 Hydration backend** (needs at least one engine emitting hooks/entries to notify on — doc 13 §4 "X1 after M1 exists") | **Backend-engine template:** plugin registered on kernel; migration **expand-contract**, **every column NOT NULL with a default**; endpoints return computed state; unit tests encode every reused notice/routing constant literally (15-min sweep, one-per-kind-per-day, 12h device window, suppress-on-live-stream) and pass; integration test vs real Postgres (idempotent sweep replay, dedup races, suppression decision); no notification/routing constant leaks outside the plugin `core/`. | 13, 04, 03, 12, 02 |
| `S-<date>-X1-backend-02-NN` | backend | **Actual delivery (net-new).** `device_tokens` storage + register/deregister endpoints (`POST/DELETE /api/v1/devices/token`); **FCM** adapter (Android/Chrome) + **APNs** adapter (Apple) driven by backend-01's routing decision; credentials (**Firebase project + `google-services.json`; APNs auth key**) loaded from the secrets vault, not code. Turns the backend-01 "suppress vs. push" decision into a real send. | backend-01; **EXTERNAL: Fady must supply the Firebase project / `google-services.json` / APNs key** (secrets vault) — likely opens **blocked** until then | **Backend-engine template** + a delivery smoke-check: `device_tokens` migration expand-contract, every column NOT NULL+default; register/deregister endpoints owner-scoped (foreign token 404 not 403); a documented end-to-end send to a real FCM + APNs token succeeds once credentials exist; no secret in code/logs. | 13, 04, 10, 02, 12 |
| `S-<date>-X1-web-01-NN` | web | **Notification center slide-over + unread badge.** Renders `['notifications']` from `/api/v1/notifications`, mark-as-read PATCH, unread count badge; ICU-renders `vars` beside the sentence; browser `Notification` API accessed **only through the web platform adapter** (so Electron can substitute). Consumes the SSE hub for live invalidation (falls back to refetch-on-focus). | backend-01; M0.5–M0.7 (web shell, theme engine, i18n engine); M1 web (card pattern) | **Client-engine template:** renders server notification state and submits **mark-read intent only**; thin-client/SSOT audit passes (no rule/window/timing logic client-side — grep finds no protocol constant); both themes render per color spec (`#FF3333` glow dark; light alert = PENDING DESIGN); Arabic RTL mirrors; strings ICU, none concatenated. | 13, 05, 01, 02, 12 |
| `S-<date>-X1-mcp-01-NN` | mcp | **`list_notifications` tool.** Returns unread notification items verbatim from `GET /api/v1/notifications` via personal-access-token auth. | backend-01; M0.8 (mcp skeleton) | **MCP template:** tool calls the real API with PAT auth, returns server state verbatim, holds no protocol logic and no AI keys; excluded tools stay excluded (no `complete_break`, no notification-*creation* tool — MCP reads notifications, never fabricates them). | 13, 11, 04 |
| `S-<date>-X1-chrome-01-NN` | chrome | **`chrome.notifications` from the service worker.** SW polls/subscribes, raises `chrome.notifications`, renders unread badge, honors server-side notification prefs (`GET/PUT /prefs/notifications`, cached for display only). Web Push/FCM registration + token handling depends on backend-02. | backend-01 (list/prefs) **+ backend-02 (for push/token registration)**; chrome M0/M1 baseline | **Client-engine template:** SW renders server-emitted notifications and submits mark-read/token intents only; thin-client audit passes (no timing/rules in SW); ICU-rendered content, Arabic included; no protocol constant in extension code. | 13, 06, 02, 12 |
| `S-<date>-X1-apple-01-NN` | apple | **Notification center screen + local schedulers + push handling.** Renders the notifications list; **schedules LOCAL notifications at server-provided fire times** (mine the legacy **11 local notification schedulers** for structure, not logic — fire times come from the API, never derived locally); APNs registration + token push to backend-02; foreground/background push handling. | backend-01 **+ backend-02 (APNs)**; apple M0/M1 baseline | **Client-engine template:** renders server notification state, schedules only at server-provided times, submits mark-read/token intents only; thin-client audit passes (no window/timing derivation in Swift); both themes; Arabic RTL mirrors; ICU strings, none concatenated. | 13, 07, 02, 12 |
| `S-<date>-X1-android-01-NN` | android | **FULL notification infrastructure (Divergence #3 — entirely missing in legacy).** `core/notifications`: `NotificationManager` + **channels** partitioned by engine concern; **WorkManager/AlarmManager** exact local alarms armed from the backend notification schedule (fire under Doze); notification center screen; **FCM** registration + token push to backend-02; fan-out to handheld (Wear/Auto consume the same layer). | backend-01 **+ backend-02 (FCM)**; android M0/M1 baseline | **Client-engine template** + explicit Divergence #3 closure: renders server notifications, arms alarms at server-provided instants only, submits mark-read/token intents; thin-client audit passes (no rules/windows in Kotlin); scheduled reminders fire under Doze at server instants; both themes on a physical device; full Arabic RTL; ICU strings, none concatenated; channel taxonomy stubbed (names PENDING DESIGN). | 13, 08, 02, 12 |
| `S-<date>-X1-desktop-01-NN` | desktop | **Native OS notifications with the window closed.** Electron main-process OS notifications via the platform adapter (substituting the web adapter), working when the main window is closed/tray-only; reuses the web notification-center components for the in-app inbox. | backend-01; **X1-web-01** (reuses components); desktop M0/M1 baseline | **Client-engine template:** renders server notifications, submits mark-read intent only; OS notification path goes through the platform adapter (no browser `Notification` in shared code); thin-client audit passes; both themes; Arabic RTL; ICU strings, none concatenated; notification copy/iconography PENDING DESIGN. | 13, 09, 05, 02 |

## Notes & gotchas

- **Legacy precedents worth reusing (reference only — greenfield rebuild, nothing ported wholesale):** `notify.go` rules and cadences (20-min caffeine, 30-min GERD, 07:00–11:00 weigh-in, one-per-kind-per-day); the `internal/notifier/` 15-min sweeper; the `internal/realtime/route.go` suppression ladder (live SSE → most-recent device 12h → primary → none, each with a `Reason`); the iOS **11 local notification schedulers** (structure to mine for apple-01, fire times still come from the server); the `notification_items` + `vars`-beside-the-sentence pattern. All of these are PENDING ratification, not spec-fixed — treat as candidate answers, confirm before coding.
- **Liability to avoid (root cause of the divergences):** never let a client compute *what/when* to notify. The engines emit; X1 fans out. `vars` are stored beside the sentence and rendered by the client's ICU catalog — **do not interpolate server-side** (Arabic reorders). A single grep for protocol constants must find nothing in any client notification code.
- **The delivery gap is the whole point:** legacy computed routing but registered **no FCM/APNs tokens**, so nothing was ever delivered. backend-02 is where that finally ships — and it is the one session gated on an **external human dependency** (Fady's Firebase project / `google-services.json` / APNs key). Expect backend-02 to open **blocked**; all client push-*handling* can be scaffolded against backend-01's list/SSE surface, with real end-to-end push verified only once backend-02 unblocks.
- **This module resolves a standing open question:** docs 02/05/06/08/09 all flagged "push provider / push-vs-poll unspecified." X1 fixes it: **per-user SSE for live suppression + FCM/APNs for push.** Update those open-question lists when backend-01/02 land.
- **PENDING DESIGN (do not invent):** notification-center UI, alert grouping, and in-app inbox presentation (all surfaces); Android channel taxonomy and user-facing channel names; desktop notification copy/iconography; **light-mode alert red** (genuinely undefined in the color spec); Auto/handheld "upcoming constraint" lead times (proposed backend-configured).
- **PENDING ratification (not design):** the reused legacy notice timings and Pomodoro intervals above — carry them as defaults, flag for Fady's sign-off.
- **Ordering:** backend-01 requires **M1 (Hydration) backend** to exist so there is a real engine emitting hooks to notify on (doc 13 §4). Within the module keep surface order backend → web → mcp → chrome → apple → android → desktop.

## Handoff reminder

At the end of **every** X1 session: write **one `session-handoff` memory** to the `healthdebug` brain — `retain` the content in the doc 13 §6 structure, then `memory/edit` to attach metadata (`category=session-handoff`, plus `project=healthdebug`, `module=X1`, `surface`, `status`, `next`, `era=greenfield`, `status(record)=current`, `title`, `tags`). Use a **unique** `source_ref: handoff/X1-notifications/<surface>/<n>` (never reuse — reuse supersedes). Then **update the single build-ledger memory** (`source_ref: build-ledger`, edited in place) so one recall shows the whole frontier and the recommended next session. Any architectural decision or new open question also gets its own classified memory and, if it changes the plan, an edit to the relevant plan doc. See doc 13 §6 and `docs/brain/BRAIN-INDEX.md`.
