Imports the notification system design folder (architecture overview, release-events/inbox foundation, APNs/FCM relay specs, outbound webhooks) and adds the Web Push spec (05, implemented in this branch), the shared outbound-email architecture note, and the v1.5 roadmap (06) covering the remaining work after APNs/FCM were deferred to v2. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
8.3 KiB
Notifications v1.5 Roadmap
Date: 2026-06-11 Status: Draft (work not started) Scope: The remaining notification work between the shipped v1 and the deferred v2 push channels. Each item is independent and sized to land as its own PR. Depends On:
00-architecture-overview.md01-release-events-and-inbox.md— implemented04-outbound-webhooks.md— implemented05-web-push.md— implemented
Where v1 landed (context for this doc)
Implemented 2026-06-11: the full foundation (availability seeding, release events,
interest index, fanout worker with burst caps, websocket channel with ticket
handshake, inbox/sync/preferences/capability APIs, web inbox + badge + settings),
outbound webhooks (Discord + generic HMAC, SSRF guard, durable outbox, retry +
auto-disable), Web Push (VAPID self-provisioned, E2E-encrypted payloads, service
worker), and the shared SMTP core (internal/mail, see
docs/architecture/email.md) with an admin Email settings page — but no feature
consuming email yet.
Deferred to v2 by explicit decision: APNs (02) and FCM (03) — they
require Silo-operated relay infrastructure and developer accounts. Also v2 per
the original plans: movie availability, aggregated notifications ("3 new
episodes"), quiet hours, cross-profile views.
1. Admin settings UI for notification controls
Why: every notifications.* setting works today but is reachable only
through the raw admin settings API. Admins should not need curl to find the
kill switches.
What: an admin settings page ("Notifications", next to the Email page added in v1) exposing:
| Group | Keys |
|---|---|
| Kill switches | notifications.release_events_enabled, notifications.fanout_enabled, notifications.ui_enabled, notifications.webhooks_enabled, notifications.web_push_enabled |
| Fanout tuning | notifications.fanout.settle_seconds (default 30), notifications.fanout.max_series_burst (default 3) |
| Webhook guards | notifications.webhooks.max_per_profile (10), notifications.webhooks.allow_private_destinations (false; dev only — label it loudly), notifications.webhooks.deliveries_per_minute_per_profile (60) |
| Retention | notifications.retention.read_days (90), notifications.retention.unread_days (180), notifications.retention.event_days (30) |
Files: add web/src/pages/admin-settings/NotificationsAdminSettings.tsx
(follow EmailSettings.tsx / useSettingsForm), register in
web/src/pages/admin-settings/AdminSettingsLayout.tsx. No backend work — all
keys are live-read.
Effort: small (one page, no migrations, no Go changes).
2. Request-fulfilled notifications (request.fulfilled)
Why: 00-architecture-overview.md calls this "the most obvious next
notification type." Users who request media currently learn it arrived by
checking manually; every delivery channel they configured should tell them.
Design: the notification_deliveries.type registry is extensible by
construction — no schema change.
- New type
request.fulfilled.reason_flagscarries the operational shape (likewebhook.auto_disableddoes), e.g.{"request_id": "...", "tmdb_id": 123, "media_type": "movie"}— never the four reason booleans. - Hook point: the request reconciliation service (
internal/mediarequests) is where a request transitions to available/fulfilled. On that transition, insert a delivery viaDeliveryRepository.InsertOperational(the path the webhook auto-disable notice already uses) and publish through the system's dispatchers so websocket, web push, and webhooks all fire. - Recipient: the requesting profile (requests are profile-attributed). No
profile_series_interestinvolvement — this is a direct, not fanned-out, notification. - Webhook enqueue: operational inserts bypass the fanout outbox, so either
(a) extend
InsertOperationalto optionally enqueue per-target attempt rows, or (b) add a small shared "dispatch one delivery durably" helper used by both this and the auto-disable notice. Prefer (b); the auto-disable notice deliberately skips webhooks (loop guard) but request notices should not. - Per-reason preferences: add nothing in v1.5. The profile master toggle
(
notification_preferences.enabled) gates it; a dedicatednotify_requestsflag can come later if users ask. - Clients: the web inbox/toast/web-push renderers fall back to a generic
card for unknown types; add a
request.fulfilledcase with the media title, poster, and a deep link to the item (or the request page until matched).
Effort: medium-small. The delivery/dispatch machinery all exists.
3. Email digest channel
Why: first real consumer of internal/mail; reaches users who don't keep a
browser open and have no webhook.
Open design decisions (resolve before building):
- Account-level, not profile-level. Email addresses live on
users; profiles have none. A digest therefore aggregates across the account's profiles (group by profile inside the email body). - Digest, not per-episode. Per-episode email is spam at hundreds-of-users scale and duplicates the realtime channels. Recommend: opt-in daily digest of unread deliveries, sent by a taskmanager task (reuse the checkpointed iteration pattern from the interest backfill), with a per-user enable + cadence setting.
- Unsubscribe / preference surface: account settings, not profile notification preferences.
Files (sketch): internal/notifications/email_digest.go (compose from
DeliveryRepository, send via mail.Sender, branch on
mail.ErrNotConfigured), a taskmanager task, a small user-settings surface.
Effort: medium. Blocked on the design decisions above, not on plumbing.
4. Native client adoption (no push required)
Why: the Android and Apple apps gain a full notification experience today — APNs/FCM only add closed-app wake-ups later.
Server surfaces ready for clients (silo-android, silo-apple):
GET /api/v1/notifications+unread-count+ read endpoints — inbox UI.GET /api/v1/notifications/sync— opaque forward cursor for reconnect/foreground catch-up (this is also the wake-fetch endpoint the v2 push specs assume, so client work done now is reused).POST /api/v1/events/ws-ticket+ticketquery param on/api/v1/events/ws,notificationschannel — realtime while the app is open. Snapshot on subscribe hydrates recent unread.GET /api/v1/notifications/capability— drive setup UI from this, never from admin settings.GET/PUT /api/v1/notifications/preferences— per-profile reason toggles.
Effort: client-repo work; the server side is done. Coordinate per the
multi-repo guidance in the repo root CLAUDE.md.
5. Hardening backlog (defer freely)
- DB-backed integration tests from the
01verification plan: idempotent availability/event inserts, cross-library delivery dedupe, per-series burst cap, outbox recovery (pending rows with no dispatch → retry worker sends), multi-node claim safety. The behaviors shipped and were exercised manually on dev; they are not yet pinned by automated tests because the repo has no Postgres test harness for this package. - Metrics:
01names Prometheus-style counters (release_events_suppressed_total, etc.); v1 ships them as structured log fields. Revisit when the repo grows a metrics registry — keep the names. - Discord embed images via the
media.discord-cdn-proxyservice (see04, "v1.5 payload"). Requires a new Silo-operated repo/service pluswebhook_image_signer.go; v1 deliberately ships text-only embeds so the user's server origin never reaches Discord. - Webhook delivery history endpoint:
webhook_delivery_attemptsalready has the listing index; aGET /api/v1/notifications/webhooks/{id}/attemptsendpoint + UI table would make failures self-diagnosable beyond the last-failure summary.
Suggested order
- Admin settings UI (#1) — smallest, completes operability.
- Request-fulfilled (#2) — highest product value per effort.
- Webhook history endpoint (#5, last bullet) — pairs naturally with #1.
- Email digest (#3) — after its design decisions are made.
- Native clients (#4) — parallel track in the client repos.