docs(android): record M7 push-notifications plan (Part 1 backend + docs)
Capture the M7 backend/docs breakdown in PLAN.md before the code lands: two event sources / one publisher, the stream catalog + PUBLIC_KINDS-gated mapping, push_devices + notification_subscriptions tables, the /auth/me routes, the SSRF endpoint guard, and the content-free-tickle ntfy service (no publish token). Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -138,6 +138,46 @@ hard reset (M3) were already in place. Biometric app-lock is **descoped from v1*
|
||||
the first `v*` release tag (once the signing secrets are set + the on-device QA pass is done) and M7
|
||||
push notifications are what remain.**
|
||||
|
||||
### M7 plan — push notifications (in progress)
|
||||
|
||||
M7 spans three repos, so it ships in **two parts**; the backend contract lands first because the app
|
||||
is a pure consumer of it (§8/§11).
|
||||
|
||||
**Part 1 — `website/` backend + `docs/` (this pass).** Additive, v1-only (new tables/routes/compose
|
||||
service; no existing response shape changes). Decision: **no ntfy publish token** — publishes go over
|
||||
the internal compose network to unguessable per-device topics carrying **content-free tickles**
|
||||
(`{ stream, ref }`); the publisher honors an optional `NTFY_PUBLISH_TOKEN` if ever set but requires
|
||||
none (keeps §11's zero-interaction promise).
|
||||
- **Two event sources, one publisher.** The fan-out is a small transport-agnostic
|
||||
`utils/pushDispatch.js` that both producers call: `utils/shardIngest.js` (`ingest()`, beside the
|
||||
existing `broadcast(event)`) for shard-derived streams, and the admin create-post path for the
|
||||
`news.post` stream (§11 lists news posts as a public stream, but they originate in the website, not
|
||||
the shard feed).
|
||||
- **Stream catalog** (`config/notificationStreams.js`): public/opt-in — `news.post`,
|
||||
`server.status`, `idoc.warning`, `champ.start`, `governor.election`; personal/owner-keyed
|
||||
(require a linked game account) — `vendor.sale`, `house.idoc`, `account.login`. `mapShardEvent()`
|
||||
maps event kinds → streams, drawing public streams **only** from the SSE `PUBLIC_KINDS` allowlist;
|
||||
sensitive kinds are never fanned out publicly. Personal events are delivered only to the owning
|
||||
user's devices, resolved via `shardLinks.getByAccount` (same ownership source as `/player/shard/*`).
|
||||
- **Tables:** `push_devices` (per-device endpoint) and `notification_subscriptions` (per-user opted-in
|
||||
streams), FK → `users` ON DELETE CASCADE, mirroring `mobile_refresh_tokens`.
|
||||
- **Routes** under the role-agnostic self surface (never `/admin`): `POST|GET /auth/me/devices`,
|
||||
`DELETE /auth/me/devices/:id`, `GET /auth/me/notifications/streams` (catalog),
|
||||
`GET|PUT /auth/me/notifications/subscriptions`. All bearer/cookie auth; Swagger regenerated.
|
||||
- **SSRF guard (important):** a device `endpoint` is a client-supplied URL the backend POSTs to, so
|
||||
registration and every publish validate it is HTTPS and its origin is in the shard's ntfy
|
||||
allow-set (`NTFY_BASE_URL` / `NTFY_ALLOWED_ORIGINS`), rejecting loopback/private hosts.
|
||||
- **ntfy** added to `website/docker-compose.yml` as a pinned upstream image with a committed
|
||||
declarative `./ntfy/server.yml` and named volume, **no published host port** (reached via the
|
||||
reverse proxy; internal-only for the publisher), anonymous read-write to unguessable topics (no
|
||||
per-user accounts — safe because tickles are content-free).
|
||||
|
||||
**Part 2 — the Android app (next pass).** UnifiedPush distributor integration, device registration
|
||||
against `POST /auth/me/devices`, the Notifications settings screen (per-stream toggles; personal
|
||||
streams greyed until a game account is linked), notification-tap deep-links, and the notification
|
||||
channel/icon. Built against the Part-1 contract; the app is architected for push from M0 (§11) so it
|
||||
adds no data-flow change to the existing screens.
|
||||
|
||||
**Prerequisite progress (§8):** all v1 prerequisites are **done** (2026-07-19) — ✅ password reset
|
||||
(item 2; website#75 + docs#8), ✅ role-agnostic `/auth/me/*` self surface (item 1; website#76 + docs#10),
|
||||
✅ version/health surfacing (item 4) and ✅ branding for mobile (item 6). **Push notifications (item 3)
|
||||
|
||||
Reference in New Issue
Block a user