7.6 KiB
Plan: Web Push on the Web Build
Goal
A user of the web build (browser tab or installed web app) can turn on the Daily Reminder and New Activity notifications and receive them, matching what native builds deliver.
Requirements:
- A
DIRECT_NOTIFICATIONpush shows its message as sent. - A
DAILY_CHECKpush shows the count of new activity for the active identity, using the same Endorser query the native fetcher uses. - The service worker never holds a long-lived credential: it holds only day-scoped JWTs, the same pool native builds hand to their background fetcher.
- The Notifications section of the Account view works on the web build.
Can FCM replace Web Push?
No. Firebase Cloud Messaging on the web (the firebase/messaging SDK) is a
layer over the browser Push API: getToken() requires a registered service
worker (firebase-messaging-sw.js by default), and that worker must display the
notification, the same as a plain Web Push worker. FCM changes only the
sending side — the push server would call the FCM HTTP v1 API instead of
sending VAPID-signed Web Push requests — and it adds a Google dependency and
Firebase config to the web build. It removes none of the work below.
Current state
public/sw.jsis registered bysrc/main.web.tson the web platform. It handles only the Web Share Target POST; it has nopushornotificationclickhandler.- The Notifications section of the Account view renders only on native
(
v-if="isNativePlatform",src/views/AccountViewView.vue:81), and the "Notification Push Server" setting is hidden withv-if="false"(src/views/AccountViewView.vue:719). Web users cannot subscribe. src/components/PushNotificationPermission.vuecontains the web subscription flow:- GET
{pushUrl}/web-push/vapidfor the VAPID key (:242); pushManager.subscribe({ userVisibleOnly: true, ... })(:538);- POST of the subscription to the relative URL
/web-push/subscribe(:583), so the web host must proxy that path to the push server; askPermission()sends the worker the placeholder secret"temporary-secret"viaSEND_LOCAL_DATA, which nothing reads.
- GET
- The push server sends a JSON payload whose
titleisDAILY_CHECK(run the new-activity check) orDIRECT_NOTIFICATION(showmessageas-is). - Account data lives in SQLite through absurd-sql, which a service worker cannot open. The worker therefore cannot decrypt the identity and sign its own JWTs.
- Native builds solve the same problem with
mintBackgroundJwtTokenPool()(src/libs/crypto/backgroundJwtPool.ts): 100 JWTs, each valid for one UTC day, stored at indexepochDay % 100, handed to the background fetcher byconfigureNativeFetcherIfReady()(src/services/notifications/nativeFetcherConfig.ts). Passkey (did:peer) identities cannot mint the pool and raiseBackgroundJwtUnsupportedIdentityError.
Work
1. Service worker push handling (public/sw.js)
push: parse the JSON payload.DIRECT_NOTIFICATION:showNotification("Direct Message", { body: message }).DAILY_CHECK(and any other title): take the token for today's slot from the pool, GET{apiServer}/api/v2/report/claimswith it, count claims newer than the storedlastNotifiedClaimId, store the newest ID, and show "There is 1 new activity" / "There are N new activities".
notificationclick: close the notification, focus an open app window or open/.- Worker state (token pool,
apiServer,activeDid,lastNotifiedClaimId) lives in the worker's own IndexedDB database, never in the app's SQLite database.
2. Hand the token pool to the worker
- On app start, identity change, and pool expiry, mint the pool with
mintBackgroundJwtTokenPool()andpostMessageit to the worker together withapiServerandactiveDid. Reuse the trigger points insrc/services/notifications/notificationAuthLifecycle.ts(onNotificationAuthMayBeReady). - Delete the
SEND_LOCAL_DATA/ placeholder-secret path inPushNotificationPermission.vue. - Passkey identities: do not offer New Activity notifications (the Daily Reminder needs no token).
3. Empty daily checks
Chrome requires every push received with userVisibleOnly: true to show a
notification. A DAILY_CHECK that finds nothing new must show one anyway, or
Chrome eventually shows its own "This site has been updated in the background"
notice. Choose one (see Open questions):
- show a quiet "No new activity" notification and close it after a short delay;
- have the push server skip pushes when there is nothing new, which requires the server to query Endorser on the user's behalf.
4. Re-enable the UI on the web build
- Render the Notifications section on web when
"PushManager" in windowand a service worker is registered, instead of only on native. - Restore the "Notification Push Server" setting for web.
- iOS Safari delivers Web Push only to web apps added to the Home Screen
(iOS 16.4+).
public/manifest.webmanifestis linked fromindex.htmland makes the app installable; the UI must explain the Home Screen requirement on iOS.
5. Hosting
- Confirm each web host (test, production, Docker
docker/*.conf) proxies/web-push/subscribeto the push server, or change the client to POST to{pushUrl}/web-push/subscribe. - Serve
sw.jswithCache-Control: no-cacheso worker updates reach users promptly.
6. Tests
- Playwright: register the worker, then deliver pushes with the Chrome DevTools
Protocol
ServiceWorker.deliverPushMessageand assert on the shown notification (registration.getNotifications()), for both payload types. - Unit-test the claim-counting function, extracted so it runs outside the worker.
Estimate
Two to four days of work, plus push-server and nginx checks (low confidence: the push server's current behavior and routing are not visible from this repository).
Open questions
- Empty daily checks: silent-notification workaround, or server-side filtering?
- Does the push server serve
/web-push/vapidand accept/web-push/subscribe, and does it sendDAILY_CHECKdaily? - Should New Activity on web use the same Endorser query as the native fetcher
(starred-plan changes) rather than
/api/v2/report/claims, so both platforms report the same thing?
Rejected
- FCM for web: still needs a service worker; adds a Google dependency to the web build and a server-side switch to the FCM API. See "Can FCM replace Web Push?".
- Reviving the old worker scripts (
sw_scripts/safari-notifications.js): they read the DexieTimeSafariIndexedDB database, which no longer holds accounts, and decrypted the identity inside the worker using a secret passed from the page, shipping NaCl and noble crypto libraries into the worker. The day-scoped token pool needs neither.
History
- 2025-02-18 (
2746b8fe): the Vite config reorganization switched the web build to a Workbox-generated worker, so the custom push, notification-click, and share-target handlers stopped shipping. - 2025-07-18 (
8bc1c521): VitePWA and the generated worker were removed; the web manifest stopped being generated and linked. - The leftover
sw_scripts/files and PWA config were deleted, andpublic/sw.jswas added for the Web Share Target only.
Checklist
- Decide the empty-daily-check behavior
- Confirm push-server endpoints and routing per environment
pushandnotificationclickhandlers inpublic/sw.js- Token-pool handoff to the worker; remove the placeholder secret
- Re-enable the web Notifications UI, with the iOS Home Screen note
- Playwright push-delivery tests