AmoleoFamily

Process & Registry

The family Web Push system — service worker, client, and prompt pattern

Amoleo-Communications (#18/#19) centralizes VAPID key management and push sending — one service, one keyring, one abuse-prevention model. What can't be centralized is the small piece of code that has to run on each product's own origin, because the browser's Push API only lets a page subscribe (or register a service worker) for itself. This doc is that piece: what's genuinely shared, what's a documented pattern instead, and why the split falls where it does.

node tools/sync-family-push.mjs       # copy family-push/*.js into every familyPush:true repo
node tools/check-family-push-sync.mjs # verify every copy still matches

1. Two files are genuinely shared; the UI prompt is a pattern, not a file

This split isn't arbitrary — it follows directly from what each piece actually is:

2. Wiring in amoleo-push-sw.js / amoleo-push-client.js

The service worker — either serve it directly as the product's own /sw.js, or importScripts('amoleo-push-sw.js') from a product's own service worker that also handles other things (asset caching, say). Every listener in the file is registered on self, so both forms work unchanged. Requires the product to serve its PWA icon at /icons/icon-192.png — a family convention the file assumes, not a parameter it takes.

The client module exports isPushSupported, registerServiceWorker, enablePushNotifications(vapidPublicKey, registerFn), revalidatePushSubscription(vapidPublicKey, registerFn), retryPushSubscription(vapidPublicKey, registerFn) for an explicit failed-setup retry, and disablePushNotifications(unregisterFn). The product supplies exactly two things:

Call revalidatePushSubscription unconditionally on every app open (it no-ops unless permission is already granted) — this is what catches a dropped iOS subscription, a rotated VAPID key, and keeps PushSubscription.lastSeenAt fresh, all in one call. Call disablePushNotifications on logout, so a shared device doesn't keep notifying whoever signed out. It returns false if the backend unregister fails and leaves the browser subscription in place for a retry; a product must surface that failure instead of silently reporting a clean logout. If a user-initiated registration fails after the browser grants permission, keep the prompt open and offer a retry through retryPushSubscription: it reuses the existing browser subscription where possible and reports relay errors to the UI instead of swallowing them.

A discrepancy the genericization found, worth knowing if you're migrating a product that had its own pre-existing push code (like Pets, #22): Amoleo-Pets' original arrayBufferToBase64 produced plain base64, not base64url. Communications' subscription API (#18) requires unpadded base64url for p256dh/auth/applicationServerKey and rejects anything else — Pets' own backend never validated the format at all, so this was invisible there. amoleo-push-client.js always encodes base64url; nothing to do, but worth knowing why a product's old code can't be assumed compatible with the new backend without this fix.

3. What the sync checker does and doesn't verify

check-family-push-sync.mjs proves file content matches — the same hash-diff idea check-family-css-sync.mjs uses. It does not verify that a product actually registers the service worker at the right path/scope, or that registerFn/unregisterFn are wired correctly — a service worker's effective scope depends on where it's served from and how .register() is called, a constraint a content hash can't see the way it can for a copied CSS file or a static component. That part is a manual review per adopting repo, same as any other repo-specific wiring code.

4. The permission-prompt pattern (spec, not a shared file)

Amoleo-Pets' PushNotificationPrompt.jsx + IOSPushNotice.jsx are the reference implementation this section writes down — the same relationship family-auth-box.md has to Connections' AuthScreen.jsx. A future product builds its own version in whatever framework it uses, following this:

Not built yet, and deliberately not attempted here: a structural checker for this pattern (the way check-auth-box.mjs validates the auth box across five real implementations). That checker earns its keep once a second product actually implements this prompt — with one reference implementation and no other adopter yet, there's nothing real to check conformance against. Revisit when #22 (Pets' migration) is followed by a second product adopting push.


Added 24 Sept 2026, during Amoleo-Communications#21's refinement and build. Amoleo-Pets is the first familyPush: true adopter (25 Sept 2026, Amoleo-Communications#22's actual cutover), and the first case needing familyPushDest as a per-file object rather than a single string — Vite serves client/public/ statically but only bundles imports under client/src/, so the service worker and client module can't share one destination directory. tools/sync-family-push.mjs and tools/check-family-push-sync.mjs both support this shape.

Generated from docs/family-push-system.md. Edit there and re-run npm run build in docs-site-src/ — never hand-edit a page.