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:
- Open the ProPR UI in Safari.
- Open Share, choose Add to Home Screen, and confirm.
- Launch ProPR from its new Home Screen icon, not the original Safari tab.
- Sign in, open Settings → Notifications → Personal notifications, and choose Enable on this browser.
- 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:
| Responsibility | Origin |
|---|---|
| UI, manifest, service worker, installed PWA, permission, browser subscription | https://app.propr.dev |
| Selected stack's REST API, Socket.IO, OAuth callback, and session | https://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 path | Required handling | Recommended 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 it | no-cache, must-revalidate |
/manifest.webmanifest | Serve as a web app manifest; never SPA-rewrite it | no-cache, must-revalidate |
/pwa-shell-assets.json | Serve the release's shell asset list | no-cache, must-revalidate |
/icons/pwa-192x192.png, /icons/pwa-512x512.png, /icons/pwa-maskable-512x512.png, /apple-touch-icon.png | Serve the real PNG files | public, max-age=86400 (purge when replaced) |
/logo.png, /logo-loading.png, /media/logo-and-name.png | Serve the real precached image files; never SPA-rewrite them | public, max-age=86400 (purge when replaced) |
/config.js | Serve the per-deployment runtime API selection; never cache or SPA-rewrite it | no-store, no-cache, must-revalidate |
/assets/* | Serve the hashed files from the same UI release | public, max-age=31536000, immutable |
/, /index.html, and application routes | Serve the current HTML; application routes may fall back to index.html | no-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
404or410automatically 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-subscriptionswhile 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/statusand/socket.io/must be reachable ont-<id>.propr.dev, while the proxy root returning404is expected. - Confirm the page and service worker come from
app.propr.dev, but notification capability/subscription requests go to the selectedt-<id>.propr.devAPI. - After changing tunnel configuration, run
propr start --restartso 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.