Files
silo-server/docs/superpowers/plans/notifications/05-web-push.md
QuickandClaude Fable 5 a24279e3e3 docs(notifications): import notification design docs, add web push + v1.5 specs
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>
2026-06-11 14:55:46 -04:00

4.0 KiB

Web Push Spec

Date: 2026-06-11 Status: Implemented Scope: Browser push notifications (Push API + VAPID) as a third push platform alongside the deferred APNs/FCM channels. Depends On:

Why Web Push ships before APNs/FCM

The architecture overview deferred mobile push because Apple and Google require pushes to official store builds to be signed by the publisher's credentials, forcing a Silo-operated relay. Web Push has neither problem:

  • No accounts, no relay. The server self-provisions a VAPID keypair on first use. Any standards-compliant browser push service (Chrome, Firefox, Edge, Safari 16+) accepts VAPID-signed requests from any origin.
  • Content-safe by protocol. Payloads are encrypted end-to-end (RFC 8291, aes128gcm) to keys held only by the subscribed browser. The vendor push service relays ciphertext. Unlike the APNs/FCM design, payloads can therefore carry full display content (titles, episode numbers, poster URLs) without violating the self-hosted privacy model — there is no opaque-wake/fetch dance.

The residual leak matches the relay threat model: the push service sees the user server's egress IP, delivery timing, and payload size. It never sees content or identity.

Data model

  • web_push_subscriptions — profile-scoped browser registrations: endpoint (unique; a resubscription from the same browser under a different profile reassigns the row), p256dh, auth, device_name, failure bookkeeping. No FK to profiles (per-user SQLite stores); profile deletion purges in code.
  • web_push_delivery_attempts — the durable dispatch outbox, mirroring webhook_delivery_attempts: pending rows enqueued in the fanout transaction, claimed post-commit with a lease, swept by the retry loop after a crash.

VAPID identity

Generated once and persisted in server_settings:

  • notifications.web_push.vapid_public_key — served to clients via the capability endpoint.
  • notifications.web_push.vapid_private_key — encrypted at rest (SensitiveSettingKeys).

The private key is persisted before the public key so a crash between writes regenerates the pair instead of stranding clients with an unusable public key. The pair must never be rotated casually: browsers bind subscriptions to it.

API surface (profile-scoped)

  • GET /api/v1/notifications/capabilityweb_push: { available, public_key }.
  • POST /api/v1/notifications/web-push/subscriptions — body is PushSubscription.toJSON() plus device_name.
  • GET /api/v1/notifications/web-push/subscriptions — for the settings UI device list.
  • DELETE /api/v1/notifications/web-push/subscriptions/{id}
  • POST /api/v1/notifications/web-push/unsubscribe — by endpoint (browsers don't know row IDs).

Subscription endpoints are attacker-controllable URLs the server will POST to, so they pass the same HTTPS + private-destination guard as webhooks, both at registration and at connect time (guarded dialer).

Delivery semantics

  • Fanout enqueues one pending attempt per enabled subscription of each recipient profile, in the same transaction as the delivery rows. No per-reason filters: profile preferences already gate delivery creation.
  • Retry schedule is short (30s/2m/10m/30m, 5 attempts): vendor push services queue messages for offline devices themselves (TTL 12h), so server-side retries only ride out transient push-service errors.
  • 404/410 from the push service is the protocol's unsubscribe signal: the subscription row is deleted, not retried.
  • notifications.web_push_enabled is the kill switch (default on).

Client

  • web/public/sw.js — displays notifications and routes clicks (episode deep link, or the inbox).
  • web/src/lib/webPush.ts — permission + subscribe/unsubscribe flows.
  • Settings → Notifications → "Browser Notifications" — this-browser toggle plus a revocable list of the profile's other subscribed devices.