# 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: 1. A `DIRECT_NOTIFICATION` push shows its message as sent. 2. A `DAILY_CHECK` push shows the count of new activity for the active identity, using the same Endorser query the native fetcher uses. 3. 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. 4. 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.js` is registered by `src/main.web.ts` on the web platform. It handles only the Web Share Target POST; it has no `push` or `notificationclick` handler. - 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 with `v-if="false"` (`src/views/AccountViewView.vue:719`). Web users cannot subscribe. - `src/components/PushNotificationPermission.vue` contains the web subscription flow: - GET `{pushUrl}/web-push/vapid` for 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"` via `SEND_LOCAL_DATA`, which nothing reads. - The push server sends a JSON payload whose `title` is `DAILY_CHECK` (run the new-activity check) or `DIRECT_NOTIFICATION` (show `message` as-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 index `epochDay % 100`, handed to the background fetcher by `configureNativeFetcherIfReady()` (`src/services/notifications/nativeFetcherConfig.ts`). Passkey (`did:peer`) identities cannot mint the pool and raise `BackgroundJwtUnsupportedIdentityError`. ## 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/claims` with it, count claims newer than the stored `lastNotifiedClaimId`, 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()` and `postMessage` it to the worker together with `apiServer` and `activeDid`. Reuse the trigger points in `src/services/notifications/notificationAuthLifecycle.ts` (`onNotificationAuthMayBeReady`). - Delete the `SEND_LOCAL_DATA` / placeholder-secret path in `PushNotificationPermission.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 window` and 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.webmanifest` is linked from `index.html` and 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/subscribe` to the push server, or change the client to POST to `{pushUrl}/web-push/subscribe`. - Serve `sw.js` with `Cache-Control: no-cache` so worker updates reach users promptly. ### 6. Tests - Playwright: register the worker, then deliver pushes with the Chrome DevTools Protocol `ServiceWorker.deliverPushMessage` and 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/vapid` and accept `/web-push/subscribe`, and does it send `DAILY_CHECK` daily? - 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 Dexie `TimeSafari` IndexedDB 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, and `public/sw.js` was added for the Web Share Target only. ## Checklist - [ ] Decide the empty-daily-check behavior - [ ] Confirm push-server endpoints and routing per environment - [ ] `push` and `notificationclick` handlers in `public/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