# S015 — SSE updates + open-app notifications Two user decisions (2026-07-31), one slice: 1. **Notification contract**: notifications are guaranteed while the app is **open**; when the app is closed we don't guarantee delivery (S014's Web Push stays, best-effort). 2. **Updates over SSE**, not polling. ## What landed - **`GET /api/events?uri=`** (BFF): an SSE stream per connection (httpz `startEventStream`, one thread each). The server polls `readSpace` every 3s **as the session user** — so the ACL stays zds's own — and emits `event: update` when the post-set hash changes; `: hb` heartbeat every ~15s keeps proxies honest and detects dead clients (write failure ends the thread). Watching zds server-side is the load-bearing choice: CLI bots post straight to zds and never touch the BFF, yet their messages still reach every open app. - **SpacePage**: the 5s `setInterval` is gone. `EventSource` drives `refresh()`; a permanently closed stream (401/403) falls back to slow polling (15s). - **Open-app notifications**: `refresh()` diffs post URIs against a baseline (first load never notifies) and raises `new Notification("林集", …)` for new posts **from others**, only when `document.hidden` — the visible app already shows the update. - **Bell semantics**: "on" now means *notification permission granted* (open-app notifications work immediately). The background push subscription is attempted in the same click but wrapped — vendor failure changes nothing. ## Verification - scenario-serve: new checkpoints — `/api/events` 401 without session; a background `curl -N` stream receives `event: update` after another member posts. PASS; unit + scenario-space green. - Real browser: alice's open app showed a message posted **by bob via the CLI** (no BFF write path involved) within one server poll cycle. - Not E2E-verified: the `document.hidden` Notification itself (headless tabs can't be truly hidden; the code path is the standard Notification API behind a permission check).