Skip to content

IMPRO_PUSH_NOTIFICATIONS

kindgracekind edited this page Aug 6, 2026 · 3 revisions

Written by Claude Fable 5

Push Notifications (impro integration)

Design for the impro-side work to deliver push notifications via a user-selectable notification service. The client/service interface is the Bluesky Push Notification Service Spec — everything impro does here is written against that spec, so it works with any conforming service and requires none to exist yet. (A reference service design exists.)

Background

impro's current notification handling is two 10-second polling loops (src/js/notificationService.js, src/js/chatNotificationService.js) that only run while a tab is open. Bluesky's own push service (courier) only serves bsky.app, so push requires pointing app.bsky.notification.registerPush at a service that actually knows impro's users — which the spec makes a user-visible choice.

Goals

  1. Users receive notifications when impro is closed — Home Screen PWA first, the Capacitor iOS app second.
  2. Users choose their notification service: a preference holding a service DID, working for services impro knows nothing about (including unauthed ones).
  3. Chat is covered, with a user choice of whether pushes carry message content.

Settings surface

The service preference holds a service DID, set under Settings → Advanced: a custom-DID field, with room for presets if known-good services emerge — mirroring how clients treat appview DIDs; there is no in-network discovery. On selection, impro resolves the DID document to the #bsky_notif endpoint and fetches the spec's config document (can happen eagerly); the settings UI renders its name.

Setting the preference surfaces an "enable push notifications" option in the Notifications settings screen, which carries the interstitial explaining the authorization: the service's name, that the grant is a separate read-only authorization to that service, and a "show message previews" choice (default on, framed like the OS lock-screen preview setting) with the disclosure that previews let the service read messages.

After enabling, the Notifications screen shows the confirmed tier from the callback echo ("Message previews: on/off") with a change action that re-walks authUrl with the other value. The echo is what keeps this display truthful — the tier is account-level, so another device may have changed it; the display self-corrects whenever the flow runs, and no push-based sync of the setting exists (accepted).

Enable flow

Uniform across authed and unauthed services:

  1. Resolve DID → fetch config.
  2. If the config has an authUrl, show the interstitial, then navigate it (login_hint, return_url, chat_previews) and handle the callback params on return per the spec.
  3. In one user gesture: Notification.requestPermission(), then pushManager.subscribe({userVisibleOnly: true, applicationServerKey: <vapidPublicKey>}).
  4. app.bsky.notification.registerPush via the PDS with the subscription JSON as token, platform: "web", appId: "social.impro".

Step 2 precedes step 3 so a user who bails at consent has not burned the one-shot permission prompt. impro treats only a successful registration as proof of a working setup, never the auth redirect alone.

The permission prompt requires a user gesture, and on iOS only works in an installed PWA — the UI needs an explicit "Add to Home Screen first" state (gated on display-mode: standalone, iOS only; desktop and Android push works from a normal tab) rather than a permission prompt that cannot appear. src/manifest.json already declares display: "standalone" with icons, so the app is installable as-is.

impro's ?aud=* OAuth scope expansion in src/oauthScopes.js already satisfies the PDS's scope assertion for the forwarded RPCs.

Registration lifecycle

  • Re-assert on every launch: registration is an idempotent upsert and there is no API to query registration state, so impro registers on every app start with an active subscription — the self-healing mechanism for rotated or lost subscriptions (mirrors social-app's native-token handling; pushsubscriptionchange is too unreliable to carry this alone, though it should also trigger a re-register).
  • Logout calls unregisterPush for the current device only, and must always do so: the service polls server-side, so nothing else stops pushes for a logged-out account from reaching the device — a real leak on shared or handed-off hardware, and the failure mode least likely to be reported. The grant persists (other devices, and grace-window reconnect per the spec).
  • Switching services is a full teardown: unregisterPush at the old service, subscription.unsubscribe(), re-subscribe with the new service's VAPID key (subscriptions are bound to the key at creation time), then the enable flow against the new service.
  • Disabling in settings is unregisterPush for the current device; server-side forgetting follows via the spec's grace-window GC. Immediate account-wide revocation is available by revoking the service's grant at the PDS, which the spec requires services to honor as full deletion.

Service worker

A service worker with no fetch handler — only push and notificationclick.

  • push: decrypt/parse the spec's payload JSON; showNotification(title, {body, tag, data: {url}}); set the badge via the Badging API when badge is present.
  • notificationclick: focus an existing client if one is open and route it to url (resolved against impro's own origin), else open the target URL.
  • Badge clearing on app open lives in the app, not the worker; the count is best-effort by design (reads on other devices cannot clear it until the next push).

Clone this wiki locally