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:
family-push/amoleo-push-sw.js(the service worker) andfamily-push/amoleo-push-client.js(the subscribe/revalidate logic) are plain, framework-agnostic JavaScript — no JSX, no product-specific import. Copying them verbatim is safe the same way copyingfamily-css/*.cssis: nothing in either file assumes anything about the product it's dropped into beyond the two integration points documented in §2. These are distributed exactly like family-css — one canonical copy here, git-copied out, checked by hash.- The permission-prompt UI (Amoleo-Pets'
PushNotificationPrompt.jsxandIOSPushNotice.jsx) is not distributed as a shared file. It's a React component, and nothing about this family's stack guarantees every product uses React — copying JSX verbatim the way family-css copies CSS would be building on an assumption the auth box's own history already rejects (family-auth-box.md: "this doc writes down what about [Connections' AuthScreen.jsx] is shared family design versus what any product wearing it is free to change" — a spec kept in prose, checked structurally, not a byte-identical file). §4 below is that spec for the push prompt, following the same idiom.
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:
vapidPublicKey— fetched from Communications'GET /internal/v1/push/vapid-key(#18), relayed through the product's own backend the same way registration is (#20 settled this: relay through the product backend, never a direct browser call to Communications).registerFn(body)/unregisterFn(endpoint)— functions the product writes, using whatever HTTP wrapper and auth scheme it already has, that POST/DELETE to its own backend's push-subscription relay endpoint (which in turn calls Communications'/internal/v1/push/ subscriptions, #18). This file never assumes a specific fetch wrapper.
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:
- The permission prompt is a shell: modal chrome, an "Enable" / "Not now" button pair, and a reassurance line ("Your browser will ask you to confirm — you can turn this off any time"). The reasons shown to the user (Pets': a short bullet list of what they'll be notified about) are the product's own content, passed in as props/children — never hardcoded into a shared component, the same way the auth box's shape is shared but its accent colour is each product's own.
- Must not confirm "enabled" until registration actually succeeds
(#20's UX review, a real gap found in Pets' original flow): call
enablePushNotificationsand await it before changing any UI state to "on" —Notification.permission, once set, can't be re-prompted by JS, so a user who granted permission but never got confirmation is stuck with no way back in except digging through browser site settings. Provide a retry affordance for a failed registration (enablePushNotificationscan be called again safely) rather than a dead end. - The iOS notice is close to 100% product-agnostic already — the explanation of iOS's Web Push limitation (no support before iOS 16.4, and only for a PWA added to the home screen, never a plain browser tab) is a platform fact true for every product verbatim, not a Pets opinion. Only the product name in the copy changes.
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.