-
Notifications
You must be signed in to change notification settings - Fork 10
IMPRO_PUSH_NOTIFICATIONS
Written by Claude Fable 5
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.)
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.
- Users receive notifications when impro is closed — Home Screen PWA first, the Capacitor iOS app second.
- Users choose their notification service: a preference holding a service DID, working for services impro knows nothing about (including unauthed ones).
- Chat is covered, with a user choice of whether pushes carry message content.
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 startUrl 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).
Uniform across authed and unauthed services:
- Resolve DID → fetch config.
- If
auth.required, show the interstitial, then navigateauth.startUrl(login_hint,return_url,chat_previews) and handle the callback params on return per the spec. - In one user gesture:
Notification.requestPermission(), thenpushManager.subscribe({userVisibleOnly: true, applicationServerKey: <vapidPublicKey>}). -
app.bsky.notification.registerPushvia the PDS with the subscription JSON astoken,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.
-
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;
pushsubscriptionchangeis too unreliable to carry this alone, though it should also trigger a re-register). -
Logout calls
unregisterPushfor 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:
unregisterPushat 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
unregisterPushfor 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.
A service worker with no fetch handler — only push and
notificationclick. Adding a fetch handler would put it in front of
the import map, the immutable asset caching, and the
functions/_middleware.js SPA-fallback correction, all of which have
sharp edges documented in CLAUDE.md. Nothing about push requires
intercepting fetches.
-
push: decrypt/parse the spec's payload JSON;showNotification(title, {body, tag, data: {url}}); set the badge via the Badging API whenbadgeis present. -
notificationclick: focus an existing client if one is open and route it tourl(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).