Date: 2026-02-15Documentation Index
Fetch the complete documentation index at: https://docs.encoreos.io/llms.txt
Use this file to discover all available pages before exploring further.
Status: All Recommendations Implemented
Reviewer: AI Agent (Cloud)
Executive Summary
iOS 16.4+ (Safari 16.4+, released March 2023) added support for Web Push Notifications in PWAs — but only when the PWA is installed to the Home Screen (running instandalone display mode). This is a critical difference from Android/desktop, where push works in-browser without installation.
After a thorough review of the Encore Health OS push notification implementation against iOS PWA requirements, 14 gaps and issues were identified, ranging from critical blockers that will prevent push from working on iOS entirely, to moderate and minor improvements.
Severity Summary
| Severity | Count | Description |
|---|---|---|
| CRITICAL | 4 | Will prevent push notifications from working on iOS |
| HIGH | 4 | Significant UX/reliability issues specific to iOS |
| MODERATE | 3 | Correctness or robustness issues |
| LOW | 3 | Best-practice improvements |
iOS PWA Push Notification Requirements (Research)
Key Requirements for iOS Push to Work
- PWA must be installed to Home Screen — Push is not available in Safari browser tabs. The app must run in
standaloneorfullscreendisplay mode via Home Screen. - User must explicitly grant permission — iOS requires a user gesture (button tap) to trigger
Notification.requestPermission(). Cannot be called on page load or in a passive context. - Service worker must be registered — The service worker that handles
pushevents must be active before subscribing. - VAPID keys required — The
applicationServerKeymust be provided duringpushManager.subscribe(). userVisibleOnly: trueis mandatory — iOS enforces that every push must show a visible notification to the user.- Payload size limit: ~4 KB — iOS Safari enforces a strict ~4 KB limit on push payloads (more restrictive than Chrome’s ~4078 bytes).
- No silent push — iOS does not support silent/background push in PWAs. Every push must display a notification.
- No
vibrateon iOS — The Vibration API is not supported on iOS. Settingvibratein notification options is silently ignored, but won’t cause errors. - Notification actions limited — iOS Safari supports a maximum of 2 notification actions (buttons). Excess actions are silently dropped.
- No
imagein notifications — iOS Safari does not support theimageoption inshowNotification(). Onlyiconandbadgeare respected. tag+renotifybehavior — iOS supportstagfor notification grouping, butrenotifybehavior may differ from Chrome.- Subscription can expire — iOS may invalidate push subscriptions when the PWA is removed from Home Screen or during OS updates. Re-subscription logic is important.
clients.openWindow()limitations — On iOS,clients.openWindow()from a notification click may not work reliably if the PWA is not already running.clients.matchAll()+client.focus()+client.navigate()is the preferred pattern.- No
requireInteraction— iOS does not supportrequireInteraction. Notifications auto-dismiss after a system-determined timeout.
Safari/iOS-Specific Behavioral Notes
- Permission prompt timing: iOS shows the native permission prompt only on explicit user gesture. If called without a gesture, it silently returns
'denied'without showing a prompt. - Subscription endpoint format: iOS uses Apple’s push service endpoints (
web.push.apple.com), which differ from Google’s FCM endpoints. Theweb-pushlibrary handles this correctly. - Service worker lifecycle: iOS Safari may terminate the service worker more aggressively than Chrome. The
pushevent handler must useevent.waitUntil()to ensure the notification is shown before termination. - No background sync for push: Unlike Android, iOS does not keep the service worker alive for background operations. Push events are the only reliable way to wake the service worker.
Gap Analysis
CRITICAL Issues (Push Will Not Work on iOS)
1. No iOS-Specific PWA Install Requirement Guidance
File:src/platform/notifications/components/PushNotificationSettings.tsx
Problem: On iOS, push notifications only work in installed PWAs (Home Screen apps). The current “not supported” message when isSupported is false says:
“Push notifications are not supported in this browser. Try using Chrome, Firefox, or Edge.”On iOS Safari (in-browser),
PushManager is not available — it only becomes available after the user installs the PWA to Home Screen. The current code will show the generic “not supported” message to all iOS Safari users, with no guidance on how to enable it.
Impact: iOS users visiting the site in Safari will be told push isn’t supported, when it actually is — they just need to install the app first.
Fix: Detect iOS Safari in-browser mode and show a specific message instructing the user to install the PWA to their Home Screen first. Example detection:
isIOSSafari && !isSupported, show:
“To enable push notifications on iOS, install Encore Health OS to your Home Screen first: tap the Share button, then ‘Add to Home Screen’.“
2. Permission Request Without User Gesture (iOS Silent Denial)
File:src/platform/notifications/hooks/usePushSubscription.ts, lines 93-101
Problem: The subscribe() function calls requestPermission() internally if permission isn’t already granted. On iOS, Notification.requestPermission() must be called in direct response to a user gesture (tap/click). If there is any async gap between the user gesture and the permission request (e.g., if subscribe() does async work before calling requestPermission()), iOS Safari will silently return 'denied' without showing the prompt.
In the current flow:
- User taps toggle switch
handleToggle(true)callssubscribe()subscribe()checksstate.permission(synchronous state from a previous render)- Calls
requestPermission()— this may or may not be within the gesture context depending on timing
'denied', requiring the user to go deep into iOS Settings to re-enable it.
Fix:
- Call
Notification.requestPermission()directly in the click handler, before any async operations. - Restructure to: request permission first (synchronously from the gesture), then subscribe.
- Consider a dedicated “Enable Notifications” button rather than a switch toggle, which makes the gesture-to-permission flow more explicit.
3. importScripts for sw-push.js May Not Load on iOS
File: vite.config.ts (line 130), public/sw-push.js
Problem: The Workbox-generated service worker uses importScripts: ['/sw-push.js'] to load the push handler. On iOS Safari, service workers have stricter requirements:
importScripts()is supported but the imported script must be served with the correct MIME type (application/javascript).- More critically, if the
sw-push.jsfile is not in the precache manifest or is cache-busted incorrectly, iOS may fail to load it when the service worker starts up after an OS/browser update. - VitePWA with Workbox’s
importScriptsoption injects the call into the generated SW, but thesw-push.jsfile is in/public/and is not precached by default with a revision hash, meaning stale cache issues are possible.
- Add
sw-push.jsto the VitePWAincludeAssetsor add a cache-busting revision to prevent stale scripts. - Better approach: inline the push handlers directly into the service worker using Workbox’s
injectManifestmode, or use VitePWA’spluginsapproach to inject custom SW code. This avoidsimportScriptsentirely. - At minimum, add
sw-push.jsto the precache manifest with a revision hash:
4. VAPID Keys Not Deployed (Configuration Blocker)
File:docs/pf/recommendations/VAPID_KEY_SETUP.md
Problem: The VAPID key setup document shows that Steps 2 and 3 (configure Supabase secrets and Vercel environment variables) are still marked as “PENDING”. Without these:
- The edge function returns 503 (“Push notifications not configured”)
- The frontend has no
VITE_VAPID_PUBLIC_KEY, sosubscribe()bails early with a toast error
- Set
VAPID_PUBLIC_KEY,VAPID_PRIVATE_KEY,VAPID_SUBJECTas Supabase edge function secrets - Set
VITE_VAPID_PUBLIC_KEYas a Vercel environment variable (all environments) - Add
VITE_VAPID_PUBLIC_KEYto.env.localfor development
HIGH Issues (Significant iOS UX/Reliability)
5. PwaInstallPrompt Uses beforeinstallprompt (Not Available on iOS)
File: src/platform/pwa/components/PwaInstallPrompt.tsx
Problem: The install prompt component relies entirely on the beforeinstallprompt event, which is a Chrome/Edge/Samsung Internet-only API. iOS Safari does not fire this event. The component will never show on iOS devices.
Since iOS push requires Home Screen installation, there is no mechanism to guide iOS users to install the PWA.
Impact: iOS users have no in-app guidance to install the PWA, which is a prerequisite for push notifications.
Fix: Add an iOS-specific install banner that detects iOS Safari (not in standalone mode) and shows manual instructions:
- “Install Encore Health OS: Tap Share → Add to Home Screen”
- Show this banner prominently on the notification settings page and optionally on first visit
6. notificationclick Handler Uses client.navigate() — Not Supported Everywhere
File: public/sw-push.js, lines 77-81
Problem: The notificationclick handler calls client.navigate(url) on an existing window client. The WindowClient.navigate() method is not part of the Service Worker spec’s guaranteed API surface — while it works on most platforms, it has issues:
- On iOS,
client.navigate()may throw or silently fail in certain conditions (e.g., when the PWA was suspended by the OS). - The code doesn’t have a
try/catcharound thenavigatecall.
7. vibrate, image, and requireInteraction Won’t Work on iOS
File: public/sw-push.js, lines 21-35
Problem: The notification options include:
vibrate: [100, 50, 100]— Not supported on iOS (Vibration API not available)image: data.image(line 39) — Not supported on iOS SafarirequireInteraction: data.priority === 'high'— Not supported on iOS
- Document this as a known iOS limitation in the notification system
- On the server/edge function side, consider platform-aware payload construction (don’t send
imagefor iOS endpoints if you’re tracking device type) - Add a
device_typeorplatformcolumn topf_push_subscriptionsto enable platform-aware notification customization
8. No Re-Subscription Logic for Expired iOS Subscriptions
File:src/platform/notifications/hooks/usePushSubscription.ts
Problem: iOS Safari may invalidate push subscriptions when:
- The user removes and re-adds the PWA to Home Screen
- iOS updates or resets the push token
- The subscription simply expires (Apple doesn’t guarantee token stability)
useEffect on mount checks if a subscription exists but doesn’t verify if it’s still valid with the push service. If the subscription has been invalidated server-side (e.g., Apple returned 410 Gone), the app will think it’s still subscribed (because the local PushManager still returns a subscription object) but pushes will fail silently.
Impact: Users on iOS may think notifications are enabled but will stop receiving them after iOS invalidates the token.
Fix:
- On mount, after finding an existing subscription, send a “verify” request to the server to check if the stored endpoint is still valid.
- If the server reports the endpoint is expired/missing, automatically re-subscribe.
- Add a periodic health check (e.g., once per session) that verifies the subscription is still active server-side.
MODERATE Issues
9. device_info Column Referenced But Doesn’t Exist on pf_push_subscriptions
File: supabase/functions/send-push-notification/index.ts, line 204
Problem: The edge function references sub.device_info when logging delivery metadata:
pf_push_subscriptions table does not have a device_info column. The table has: id, user_id, endpoint, p256dh_key, auth_key, user_agent, created_at, last_used_at, organization_id, custom_fields, updated_at, created_by, updated_by.
The device_info column exists on hr_time_clock_punches — a completely different table.
Impact: sub.device_info will always be undefined, so the metadata will always log 'unknown'. This is a data quality issue for push delivery analytics.
Fix: Use sub.user_agent instead:
10. web-push Library Import via esm.sh May Have Deno Compatibility Issues
File: supabase/functions/send-push-notification/index.ts, line 8
Problem: The import uses:
web-push library is a Node.js library that relies on Node’s crypto module for ECDH key exchange and HKDF. While esm.sh can shimmy some Node APIs, the web-push library’s cryptographic operations may not work correctly in Deno’s edge function runtime because:
web-pushinternally usescrypto.createECDH(),crypto.createHmac(), etc. — Node-specific APIs- Deno’s Node compatibility layer covers many of these, but edge function environments may have restrictions
- The
esm.shCDN may not correctly polyfill all Node crypto primitives for Deno
web-push library fails at runtime, push notifications will fail silently with a cryptic error.
Impact: Push sending may fail in production with crypto-related errors that are hard to debug.
Fix:
- Test the edge function in a local Deno environment to verify
web-pushworks - Consider using
npm:web-push@3.6.7import specifier instead (Supabase edge functions supportnpm:specifiers which have better Node compatibility) - Actually, I see the VAPID_KEY_SETUP.md recommends
npm:web-push@3.6.7but the actual file useshttps://esm.sh/web-push@3.6.7— thenpm:specifier is preferred for Supabase edge functions
11. send-push-notification Edge Function Has No Authentication/Authorization
File: supabase/functions/send-push-notification/index.ts
Problem: The edge function accepts a user_id in the request body and sends push notifications to that user’s devices. There is no authentication check — unlike save-push-subscription and remove-push-subscription which both validate the JWT token, this function blindly trusts the user_id parameter.
This means:
- Any authenticated user could send push notifications to any other user
- If the function is somehow exposed, unauthenticated calls could send notifications
user_id.
Fix: Add authentication and authorization:
LOW Issues
12. console.log Statements in Service Worker and Edge Functions
Files: public/sw-push.js, supabase/functions/send-push-notification/index.ts
Problem: The codebase has multiple console.log and console.error statements. Per the project’s coding standards, console statements are prohibited in production code.
Note: Edge functions use createLogger() from shared utilities but also have raw console.log/console.warn/console.error calls. The service worker (sw-push.js) uses console.log/console.error directly.
Fix:
- Replace console statements in
sw-push.jswith structured logging or remove them - Ensure the edge function uses only the logger utility consistently
13. Push Notification Settings “Learn More” Links to Chrome-Only Documentation
File:src/platform/notifications/components/PushNotificationSettings.tsx, line 134
Problem: The “Learn more” link about installing as an app points to https://support.google.com/chrome/answer/9658361 — Chrome’s documentation. This is unhelpful for iOS Safari users.
Fix: Either:
- Make the link platform-aware (detect iOS/Android/desktop and link to the appropriate documentation)
- Link to your own documentation page that covers all platforms
14. PushNotificationSettings “Permission Denied” Recovery Instructions Are Chrome-Specific
File: src/platform/notifications/components/PushNotificationSettings.tsx, lines 60-67
Problem: When permission is 'denied', the instructions say:
- Click the lock icon in your browser’s address bar
- Find “Notifications” in the site settings
- Change from “Block” to “Allow”
- Refresh this page
- Open Settings app
- Scroll to Safari (or the PWA name)
- Tap Notifications
- Enable notifications
Summary of Recommended Actions
Immediate (Before Shipping iOS Push)
| # | Issue | Severity | Status |
|---|---|---|---|
| 4 | Deploy VAPID keys to Supabase & Vercel | CRITICAL | Manual config required (see VAPID_KEY_SETUP.md) |
| 1 | Add iOS install-first guidance on push settings page | CRITICAL | DONE |
| 2 | Fix permission request to be gesture-bound for iOS | CRITICAL | DONE |
| 3 | Fix sw-push.js loading reliability (add to precache or inline) | CRITICAL | DONE |
| 11 | Add auth/authz to send-push-notification edge function | MODERATE | DONE |
Short-Term (Improve iOS Experience)
| # | Issue | Severity | Status |
|---|---|---|---|
| 5 | Add iOS-specific PWA install prompt | HIGH | DONE |
| 6 | Add try/catch to notificationclick navigate | HIGH | DONE |
| 8 | Add subscription re-validation on mount | HIGH | DONE |
| 10 | Switch web-push import to npm: specifier | MODERATE | DONE |
| 9 | Fix device_info reference to user_agent | MODERATE | DONE |
Longer-Term (Polish)
| # | Issue | Severity | Status |
|---|---|---|---|
| 7 | Document iOS notification option limitations, consider platform-aware payloads | HIGH | DONE |
| 12 | Remove/replace console statements | LOW | DONE |
| 13 | Make “Learn more” link platform-aware | LOW | DONE |
| 14 | Make permission-denied instructions platform-aware | LOW | DONE |
Additional Recommendations
Platform Detection Utility
Create a shared utility for iOS/platform detection that can be used across the notifications and PWA modules:iOS Push Test Checklist
Before claiming iOS push support is ready:- VAPID keys deployed to Supabase secrets and Vercel env vars
- PWA installed to Home Screen on iOS 16.4+ device
- Permission prompt appears on button tap (not on page load)
- Subscription is created and stored in
pf_push_subscriptions - Test notification received and displayed on iOS
- Notification tap opens the app and navigates to correct URL
- Notification works when app is in background (not running)
- Subscription survives app restart
- Expired subscription is detected and user is prompted to re-subscribe
- Edge function correctly handles Apple push service endpoints