Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.encoreos.io/llms.txt

Use this file to discover all available pages before exploring further.

Date: 2026-02-15
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 in standalone 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

SeverityCountDescription
CRITICAL4Will prevent push notifications from working on iOS
HIGH4Significant UX/reliability issues specific to iOS
MODERATE3Correctness or robustness issues
LOW3Best-practice improvements

iOS PWA Push Notification Requirements (Research)

Key Requirements for iOS Push to Work

  1. PWA must be installed to Home Screen — Push is not available in Safari browser tabs. The app must run in standalone or fullscreen display mode via Home Screen.
  2. 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.
  3. Service worker must be registered — The service worker that handles push events must be active before subscribing.
  4. VAPID keys required — The applicationServerKey must be provided during pushManager.subscribe().
  5. userVisibleOnly: true is mandatory — iOS enforces that every push must show a visible notification to the user.
  6. Payload size limit: ~4 KB — iOS Safari enforces a strict ~4 KB limit on push payloads (more restrictive than Chrome’s ~4078 bytes).
  7. No silent push — iOS does not support silent/background push in PWAs. Every push must display a notification.
  8. No vibrate on iOS — The Vibration API is not supported on iOS. Setting vibrate in notification options is silently ignored, but won’t cause errors.
  9. Notification actions limited — iOS Safari supports a maximum of 2 notification actions (buttons). Excess actions are silently dropped.
  10. No image in notifications — iOS Safari does not support the image option in showNotification(). Only icon and badge are respected.
  11. tag + renotify behavior — iOS supports tag for notification grouping, but renotify behavior may differ from Chrome.
  12. 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.
  13. 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.
  14. No requireInteraction — iOS does not support requireInteraction. 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. The web-push library handles this correctly.
  • Service worker lifecycle: iOS Safari may terminate the service worker more aggressively than Chrome. The push event handler must use event.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:
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) || 
  (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
const isStandalone = window.matchMedia('(display-mode: standalone)').matches || 
  (window.navigator as any).standalone === true;
const isIOSSafari = isIOS && !isStandalone;
When 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:
  1. User taps toggle switch
  2. handleToggle(true) calls subscribe()
  3. subscribe() checks state.permission (synchronous state from a previous render)
  4. Calls requestPermission() — this may or may not be within the gesture context depending on timing
Impact: On iOS, the permission prompt may never appear, and permission will be silently set to '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.js file 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 importScripts option injects the call into the generated SW, but the sw-push.js file is in /public/ and is not precached by default with a revision hash, meaning stale cache issues are possible.
Impact: Push event listener may not be registered in the service worker on iOS, causing push messages to be received but silently dropped (no notification shown). Fix:
  • Add sw-push.js to the VitePWA includeAssets or add a cache-busting revision to prevent stale scripts.
  • Better approach: inline the push handlers directly into the service worker using Workbox’s injectManifest mode, or use VitePWA’s plugins approach to inject custom SW code. This avoids importScripts entirely.
  • At minimum, add sw-push.js to the precache manifest with a revision hash:
additionalManifestEntries: [
  { url: '/index.html', revision: buildId },
  { url: '/sw-push.js', revision: buildId },  // Add this
],

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, so subscribe() bails early with a toast error
Impact: Push notifications cannot work at all — on any platform, not just iOS. Fix: Deploy the VAPID keys:
  1. Set VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT as Supabase edge function secrets
  2. Set VITE_VAPID_PUBLIC_KEY as a Vercel environment variable (all environments)
  3. Add VITE_VAPID_PUBLIC_KEY to .env.local for 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 ShareAdd 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/catch around the navigate call.
Impact: Clicking a notification on iOS may not navigate to the correct page, and an unhandled error could prevent the notification click from being processed. Fix:
// Wrap navigate in try/catch, fall back to openWindow
try {
  await client.focus();
  await client.navigate(url);
} catch (e) {
  // Fallback: open a new window
  if (clients.openWindow) {
    await clients.openWindow(url);
  }
}

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 Safari
  • requireInteraction: data.priority === 'high' — Not supported on iOS
While these won’t cause errors (they’re silently ignored), they create a false expectation that high-priority notifications will persist on iOS and that images will display. Impact: Notifications on iOS will look different from what’s configured — no images, no persistent display for “high” priority, no vibration. Fix:
  • 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 image for iOS endpoints if you’re tracking device type)
  • Add a device_type or platform column to pf_push_subscriptions to 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)
The current 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:
metadata: { device: sub.device_info || 'unknown' }
However, the 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:
metadata: { device: sub.user_agent || 'unknown' }

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:
import webPush from 'https://esm.sh/web-push@3.6.7';
The 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:
  1. web-push internally uses crypto.createECDH(), crypto.createHmac(), etc. — Node-specific APIs
  2. Deno’s Node compatibility layer covers many of these, but edge function environments may have restrictions
  3. The esm.sh CDN may not correctly polyfill all Node crypto primitives for Deno
If the 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-push works
  • Consider using npm:web-push@3.6.7 import specifier instead (Supabase edge functions support npm: specifiers which have better Node compatibility)
  • Actually, I see the VAPID_KEY_SETUP.md recommends npm:web-push@3.6.7 but the actual file uses https://esm.sh/web-push@3.6.7 — the npm: 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:
  1. Any authenticated user could send push notifications to any other user
  2. If the function is somehow exposed, unauthenticated calls could send notifications
Impact: Security vulnerability — any user can spam any other user with push notifications by calling the edge function with an arbitrary user_id. Fix: Add authentication and authorization:
// Verify the caller is authenticated
const authHeader = req.headers.get('Authorization');
// For user-to-user: verify caller has permission to notify target user
// For system: verify service role key is used
At minimum, check that the request comes from an authenticated user or service role. For user-initiated notifications, verify the caller has the right to notify the target user (e.g., same organization, has admin role).

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.js with structured logging or remove them
  • Ensure the edge function uses only the logger utility consistently

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:
  1. Click the lock icon in your browser’s address bar
  2. Find “Notifications” in the site settings
  3. Change from “Block” to “Allow”
  4. Refresh this page
These instructions are Chrome-specific. On iOS Safari, the process is:
  1. Open Settings app
  2. Scroll to Safari (or the PWA name)
  3. Tap Notifications
  4. Enable notifications
Fix: Detect the platform and show platform-appropriate instructions for re-enabling notifications.

Immediate (Before Shipping iOS Push)

#IssueSeverityStatus
4Deploy VAPID keys to Supabase & VercelCRITICALManual config required (see VAPID_KEY_SETUP.md)
1Add iOS install-first guidance on push settings pageCRITICALDONE
2Fix permission request to be gesture-bound for iOSCRITICALDONE
3Fix sw-push.js loading reliability (add to precache or inline)CRITICALDONE
11Add auth/authz to send-push-notification edge functionMODERATEDONE

Short-Term (Improve iOS Experience)

#IssueSeverityStatus
5Add iOS-specific PWA install promptHIGHDONE
6Add try/catch to notificationclick navigateHIGHDONE
8Add subscription re-validation on mountHIGHDONE
10Switch web-push import to npm: specifierMODERATEDONE
9Fix device_info reference to user_agentMODERATEDONE

Longer-Term (Polish)

#IssueSeverityStatus
7Document iOS notification option limitations, consider platform-aware payloadsHIGHDONE
12Remove/replace console statementsLOWDONE
13Make “Learn more” link platform-awareLOWDONE
14Make permission-denied instructions platform-awareLOWDONE

Additional Recommendations

Platform Detection Utility

Create a shared utility for iOS/platform detection that can be used across the notifications and PWA modules:
// src/platform/pwa/utils/platformDetection.ts
export function isIOS(): boolean {
  return /iPad|iPhone|iPod/.test(navigator.userAgent) || 
    (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
}

export function isStandalonePWA(): boolean {
  return window.matchMedia('(display-mode: standalone)').matches || 
    (window.navigator as any).standalone === true;
}

export function isIOSSafariInBrowser(): boolean {
  return isIOS() && !isStandalonePWA();
}

export function canReceivePushNotifications(): boolean {
  // iOS: only in installed PWA
  if (isIOS()) {
    return isStandalonePWA() && 'PushManager' in window;
  }
  // Other platforms: check standard APIs
  return 'Notification' in window && 'serviceWorker' in navigator && 'PushManager' in window;
}

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

References