Skip to main content

PWA, Web Push, and Badges

ProPR's production UI is an installable Progressive Web App (PWA). Web Push is optional: the Inbox and live UI continue to work when Push is unavailable or disabled or a browser lacks Push or Badging APIs. Outside localhost, both the UI and API must be reached over HTTPS.

Web Push delivers text notifications and may display them while the PWA is backgrounded. It never carries briefing audio or starts speech playback. Voice Briefings fetch text only when a signed-in user requests it and use browser/OS speech capabilities only in the visible app; backgrounding or locking the device cancels listening and playback.

Configure VAPID​

Web Push is ready automatically after successful API startup. No VAPID environment variables are required. Users still choose Enable on this browser, accept the browser permission prompt, and opt into their desired Push categories. Automatic instance setup never enables a user or category.

The API generates one P-256 pair and stores it in web-push/vapid.json beside the SQLite database: the parent of DB_FILENAME, otherwise DATA_DIR, otherwise ./data. This uses the existing instance data mount: the CLI/launcher stack's data/, development Compose's ./data, and production Compose's propr-sqlite-data volume. Keep that mount across upgrades and container recreation. All API processes for an instance must share it. Creation publishes a complete, synced file atomically without overwriting another initializer's pair. Routes and delivery use the same configuration resolved during startup; reading capabilities does not create secrets or open a database.

The default subject is the origin of the configured public HTTPS API_PUBLIC_URL, then HTTPS FRONTEND_URL. Local/non-HTTPS installations fall back to the project's public contact URL, https://propr.dev. Request Host headers are never used. You can override the subject alone with WEB_PUSH_VAPID_SUBJECT=https://your-instance.example.com or a valid mailto: contact. Changing the subject or instance URL does not rotate keys.

Explicit keys and disabling Push​

Existing valid manual keys continue to work. Supply both WEB_PUSH_VAPID_PUBLIC_KEY and WEB_PUSH_VAPID_PRIVATE_KEY from the same P-256 pair in the protected stack .env or secret manager. The subject is optional and uses the same automatic default. An explicit pair takes precedence without writing or replacing the automatic file. Removing both overrides resumes the previously stored automatic pair, or creates one if none existed. Switching between different identities requires browsers to subscribe again.

propr check and both launcher paths accept automatic and subject-only modes; they reject partial pairs, malformed subjects/keys and mismatched pairs without printing their values. They validate configuration without generating secrets; storage setup/diagnostics occur at API startup. Direct API startup fails closed for Push on these errors while unrelated API features remain available.

WEB_PUSH_ENABLED=false skips automatic key setup and disables enrollment and delivery without deleting preferences, subscriptions or stored keys. Launcher checks still reject invalid explicit settings, even while Push is disabled.

The public key is intentionally browser-visible through authenticated GET /api/notifications/config. The private key is a server signing credential: never commit it, copy it into VITE_*/config.js, put it on a command line, paste it into an issue, or print it in logs. Automatic storage uses directory mode 700 and file mode 600; only the service account should have access. It uses filesystem protection independently of session/OAuth encryption secrets, so rotating those secrets cannot invalidate the Push identity. Protect backups as credentials too.

Backup, restore and deliberate rotation​

Back up the whole instance data directory, including web-push/vapid.json, along with the database. Restore the pair unchanged to the same relative location with its restrictive permissions and service-account ownership. Manual installations must also preserve their environment/secret-manager pair. Never display private file contents to troubleshoot.

On invalid/corrupt storage or persistence failure, Push stays unavailable and the API emits a sanitized actionable diagnostic. It does not overwrite corrupt keys or advertise a temporary key. Repair permissions, disk space or mount access, or restore a known-good backup, then restart the API to retry. A failure after atomic publication retains the same complete pair for the next startup. Private temporary files left by a killed initializer can be removed while all API processes are stopped; they are never used as the active identity.

To deliberately rotate after exposure, stop all API processes, securely archive or remove the automatic web-push/vapid.json, and restart to generate a new pair. For manual mode, replace both environment keys instead. Ensure no old API process is still signing with the previous pair. Every affected browser must disable and re-enable notifications to create a new subscription; user/category preferences are preserved. Deleting the data volume or removing manual overrides can also change the active identity, so neither is a routine restart procedure.

The optional dispatcher interval, batch, lease, request-timeout, TTL, attempt, and retry variables are listed in Configuration Reference. After configuration changes, run propr check and propr start --restart.

For everyday Inbox actions and repository suppression, see Inbox and notifications.

Install and enable notifications​

In a supported desktop or Android browser, install ProPR from the browser's install control, open the installed app, then go to Settings → Notifications → Personal notifications → Enable on this browser. Permission is requested only from that button click. Enable Push for the desired categories after the browser is subscribed.

On iOS or iPadOS 16.4 and later, Web Push is available only to a Home Screen web app:

  1. Open the ProPR UI in Safari.
  2. Open Share, choose Add to Home Screen, and confirm.
  3. Launch ProPR from its new Home Screen icon, not the original Safari tab.
  4. Sign in, open Settings → Notifications → Personal notifications, and choose Enable on this browser.
  5. Accept the system notification prompt, then enable the desired Push categories.

Apple describes the platform behavior in Web Push for Web Apps on iOS and iPadOS. A permission prompt cannot be triggered silently; it must follow the user's action. If permission is denied, ProPR cannot ask again until it is allowed in browser/site or OS notification settings.

Each browser profile or installed app creates its own subscription. Enabling one device does not enable another.

UI and API origins​

The PWA, service worker, notification permission, and browser subscription belong to the UI origin. The API stores the subscription for the authenticated ProPR user and uses the installation's private VAPID key to send to it.

Self-hosted UI​

For a normal self-hosted deployment, the UI origin is the public origin serving the propr/ui bundle, such as https://propr.example.com. The API may be same-origin or a separate HTTPS origin. FRONTEND_URL must equal the UI origin, while API_PUBLIC_URL and runtime PROPR_UI_PUBLIC_API_URL must identify the browser-reachable API origin. Moving the UI to a different scheme, host, or port creates a different PWA origin; users must install/subscribe on the new origin.

Hosted UI tunnel​

With the hosted tunnel, the origins are deliberately different:

ResponsibilityOrigin
UI, manifest, service worker, installed PWA, permission, browser subscriptionhttps://app.propr.dev
Selected stack's REST API, Socket.IO, OAuth callback, and sessionhttps://t-<id>.propr.dev

The t-<id> tunnel routes only /api/* and /socket.io/*; it does not serve PWA files. The subscription is created by the app.propr.dev service worker, then registered with the currently selected t-<id> backend and owned there by the authenticated user. A browser has one Push subscription for that service-worker registration. Before changing the hosted UI to a different ProPR stack, disable Push on the old stack, switch stacks, and enable it again so the new stack owns a subscription created with its VAPID public key. Do not copy subscription endpoints between users, stacks, origins, or browser profiles.

See Hosted UI Tunnel for tunnel routing and authentication details.

Reverse proxy and caching contract​

The proxy must preserve HTTPS, content types, and these exact UI-origin paths. Exact file locations must win over the SPA fallback; returning index.html for a worker, manifest, icon, shell manifest, or config.js breaks installation or can send the browser to the wrong API.

UI-origin pathRequired handlingRecommended Cache-Control
/service-worker.js (the ProPR worker; often named sw.js in other deployments)Serve JavaScript from the UI root with scope /; never SPA-rewrite itno-cache, must-revalidate
/manifest.webmanifestServe as a web app manifest; never SPA-rewrite itno-cache, must-revalidate
/pwa-shell-assets.jsonServe the release's shell asset listno-cache, must-revalidate
/icons/pwa-192x192.png, /icons/pwa-512x512.png, /icons/pwa-maskable-512x512.png, /apple-touch-icon.pngServe the real PNG filespublic, max-age=86400 (purge when replaced)
/logo.png, /logo-loading.png, /media/logo-and-name.pngServe the real precached image files; never SPA-rewrite thempublic, max-age=86400 (purge when replaced)
/config.jsServe the per-deployment runtime API selection; never cache or SPA-rewrite itno-store, no-cache, must-revalidate
/assets/*Serve the hashed files from the same UI releasepublic, max-age=31536000, immutable
/, /index.html, and application routesServe the current HTML; application routes may fall back to index.htmlno-cache, must-revalidate

The published UI container already supplies strict headers for /service-worker.js and /config.js; an outer CDN or proxy must preserve them or apply an equally strict policy. Do not apply an immutable blanket policy to the worker, manifest, shell manifest, HTML, or runtime config. WebSocket upgrades remain required on the API's /socket.io/ path.

Check the public response, not only the container upstream:

curl -I https://propr.example.com/service-worker.js
curl -I https://propr.example.com/manifest.webmanifest
curl -I https://propr.example.com/icons/pwa-192x192.png
curl -I https://propr.example.com/config.js

Expect 200, the correct non-HTML content type, and the cache policy above. In the browser, confirm the registered worker's script URL and scope are the UI origin and /.

Badges are progressive enhancement​

The Show an unread-count badge on the installed app preference asks the browser's Badging API to show the Inbox unread count, capped at 99. Push receipt and foreground Inbox refresh both update it; reading or dismissing notifications reduces or clears it.

Badge presentation and support vary by browser, operating system, launcher, and installation state. Some environments show a number, some show only a dot, and unsupported browsers show nothing. ProPR treats setAppBadge/clearAppBadge as best effort: missing or rejected badge calls never block notification delivery or Inbox use. Test badges on an installed PWA rather than assuming a normal browser tab will display them.

Troubleshooting​

Permission is denied​

  • Open the browser's site settings and the operating system's notification settings for the UI origin (or installed ProPR app), allow notifications, then reload/reopen the app.
  • On iPhone/iPad, confirm iOS/iPadOS 16.4+ and that ProPR was launched from its Home Screen icon before pressing Enable on this browser.
  • If policy-managed browser settings block notifications, an administrator must change that policy; repeatedly pressing Enable cannot reopen a denied prompt.

Subscription is missing or expired​

  • In Settings, confirm the browser says This browser is subscribed. Disable and re-enable it to create a fresh endpoint.
  • Provider responses 404 or 410 automatically revoke and erase an expired subscription. Re-enable Push on that browser afterward.
  • If the UI origin or VAPID pair changed, unsubscribe/resubscribe; old subscriptions cannot be reused with a new origin or signing identity.
  • Check GET /api/notifications/push-subscriptions while authenticated and the API logs. Do not put an endpoint URL or private key in a ticket or log; endpoints are bearer-like capability URLs.

Notification arrives but the badge is missing​

  • Confirm the badge preference is enabled and the Inbox has unread items.
  • Use an installed PWA on an OS/browser that implements the Badging API. A missing badge on an unsupported platform is expected and does not indicate Push failure.
  • Open the app once and compare the Inbox unread count; foreground refresh should reconcile the badge. OS launchers may render a dot instead of the numeric count.

Hosted tunnel cannot connect​

  • Run propr tunnel verify; /api/status and /socket.io/ must be reachable on t-<id>.propr.dev, while the proxy root returning 404 is expected.
  • Confirm the page and service worker come from app.propr.dev, but notification capability/subscription requests go to the selected t-<id>.propr.dev API.
  • After changing tunnel configuration, run propr start --restart so the API, worker, UI selection, CORS, and public URLs agree.
  • If switching instances, disable Push before switching and re-enable it on the destination stack.

See Troubleshooting for full tunnel diagnostics.

The release-verification checklist for PWA installation, Web Push delivery and badges lives in the repository's docs/voice-and-push-release-verification.md.