Push notifications for PWA on iOS are one of the most discussed topics
among web developers in the last two years. Apple kept this
feature closed for a long time, and when it opened it, it did so with restrictions
that still raise questions in practice.
In this article — a technical breakdown based on a real Kazki AI project:
what works, what doesn't, and what code is needed to make everything work on iPhone.
If you are still choosing between PWA and a native app — first read
PWA vs Native in 2026: comparison and a real case
.
📚 Article Contents
🎯 Section 1. Why iOS blocked Push Notifications for PWA for years
Short answer: until March 2023, the Push API for PWA on iOS
was deliberately disabled — regardless of the platform's technical capabilities.
Android supported Web Push since 2015. Apple added support
only in iOS 16.4 (March 2023) — eight years later. The reason is not technical,
but business-related: PWA with full-fledged pushes reduces the incentive to publish
apps in the App Store, where Apple charges a 15–30% commission.
Before iOS 16.4, web applications on iPhone physically could not receive
push notifications — not due to WebKit limitations, but due to Apple's deliberate
decision not to implement this feature.
Officially, Apple explained the lack of support with privacy and UX considerations. Technically, the implementation indeed required
integration with APNs (Apple Push Notification service) in a browser
context — but this was not an insurmountable obstacle, as macOS Safari
supported Web Push since OS X Mavericks (2013).
[Wikipedia: Apple Push Notification service]
Real pressure came from outside. EU regulatory requirements under the
Digital Markets Act forced Apple to reconsider its approach to web applications.
In February 2023, Apple even planned to limit PWA functionality
in the EU to the level of a "web bookmark" — but after public pressure from developers,
it backed down and instead added Web Push for all regions simultaneously.
[Brainhub: PWA on iOS — Current Status]
🎯 Section 2. iOS 16.4, 17, 18: chronology of changes — what appeared and what is still broken
Each iOS version improved PWA Push support,
but the problem with random subscription "disappearance" is still not fully resolved.
iOS 16.4 opened Web Push for installed PWAs. iOS 17 enabled
the relevant APIs by default in all browsers. iOS 18 did not add
radical changes, but developers still report cases where
push subscriptions "disappear" without an obvious reason.
iOS 16.4 is a start, not the finish. Each subsequent update
partially corrected the behavior, but the stability of PWA Push
on iOS still lags behind Android.
iOS 16.4 (March 2023) — opening
The first release with Web Push support for Home Screen web apps.
Push notifications appeared on the Lock Screen, in the Notification Center
and on Apple Watch. Badging API support was added.
[WebKit: Web Push for Web Apps on iOS and iPadOS]
But a problem immediately emerged: push subscriptions stopped working
after 1–2 weeks. The endpoint became inactive, and the user
had to reinstall the PWA to subscribe again.
[Apple Developer Forums: PWA Push Issues iOS]
iOS 17 (September 2023) — expansion
Web Push API became available by default in all browsers on iOS —
not just in Safari PWA. Stability improved, but the problem
of disappearing subscriptions remained for many developers.
[OneSignal: iOS Web Push Setup]
iOS 18 (September 2024) — stabilization
There were no significant changes in the Web Push API. Some developers report
improved delivery reliability, while others still experience the same problems with lost
subscriptions after prolonged app inactivity.
[Apple Developer Forums: iOS 17–18 discussion]
🎯 Section 3. Main condition: Safari only + Add to Home Screen — Chrome doesn't count
Push notifications on iOS work exclusively
for PWAs installed via Safari → Share → Add to Home Screen.
An open tab in any browser does not count.
Unlike Android, where Chrome can request push permissions
directly in the browser, iOS requires the PWA to be installed as
a separate app on the home screen. Chrome on iOS uses WebKit
under the hood, and it cannot replace Safari in this process.
Web Push is not supported inside Safari on iOS — it only works
for Home Screen web apps.
[Apple Developer Forums]
This means that before requesting notification permission,
you need to check two things: whether it's iOS, and whether the app is opened
in standalone mode (i.e., installed on the home screen).
const isIOS = /iphone|ipad/i.test(navigator.userAgent);
const isStandalone = window.matchMedia('(display-mode: standalone)').matches;
if (isIOS && !isStandalone) {
// Show hint: "Add to home screen to receive notifications"
showInstallPrompt();
return;
}
If you skip this check — Notification.requestPermission()
on iOS will simply return 'default' or throw an error,
and the user won't understand why the "subscribe" button isn't working.
🎯 Section 4. Step-by-step: user subscription to Push on iOS
Subscription on iOS requires a user gesture —
the permission request cannot be triggered automatically on page load,
only in response to a user action.
Unlike Android, where permission can be requested programmatically via
setTimeout, iOS Safari strictly requires Notification.requestPermission()
to be called directly from a click handler. Otherwise, the request will be silently
ignored or blocked.
[pwa.io: Web Push with iOS Safari 16.4]
"Remember, you cannot request a push subscription without
an explicit user gesture."
[Apple WWDC22: Meet Web Push for Safari]
Here is the full subscription flow from a real Kazki AI project:
const VAPID_PUBLIC_KEY = 'YOUR_PUBLIC_VAPID_KEY';
async function initPushSubscription() {
// 1. Check for support
if (!('serviceWorker' in navigator) || !('PushManager' in window)) return;
// 2. Check for iOS: standalone mode only
const isIOS = /iphone|ipad/i.test(navigator.userAgent);
const isStandalone = window.matchMedia('(display-mode: standalone)').matches;
if (isIOS && !isStandalone) {
showInstallPrompt(); // show "Add to home screen" hint
return;
}
try {
const reg = await navigator.serviceWorker.ready;
// 3. Check if already subscribed
const existing = await reg.pushManager.getSubscription();
if (existing) return;
// 4. Request permission — only after user gesture!
const permission = await Notification.requestPermission();
if (permission !== 'granted') return;
// 5. Create subscription
const subscription = await reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY)
});
// 6. Save to server
await fetch('/api/push/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
endpoint: subscription.endpoint,
p256dh: subscription.toJSON().keys.p256dh,
auth: subscription.toJSON().keys.auth
})
});
} catch (e) {
console.error('Push error:', e.message);
}
}
// Subscribe only on click — not automatically!
document.querySelector('#push-btn')
.addEventListener('click', initPushSubscription);
The subscription endpoint on iOS always starts with
https://web.push.apple.com/, on Android — with
https://fcm.googleapis.com/. This is useful for diagnostics.
[pqvst.com: Demystifying Web Push Notifications]
🎯 Section 5. Web App Manifest and Service Worker: mandatory settings
without display: standalone
in manifest.json, the Push API on iOS simply won't appear in the Service Worker,
even if everything else is configured correctly.
Apple requires the PWA to be declared as a "Home Screen web app" via
the manifest. Without this field, registration.pushManager
returns undefined on iOS.
[GitHub: webpush-ios-example]
PushManager appears in the serviceWorker only after adding the site
to the home screen via Safari — and only if the manifest contains
display: standalone.
Minimum manifest.json for Push to work on iOS:
{
"name": "Kazki AI",
"short_name": "Kazki",
"display": "standalone",
"start_url": "/",
"scope": "/",
"background_color": "#ffffff",
"theme_color": "#6366f1",
"icons": [
{
"src": "/images/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/images/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
Critically important: icons must be real PNG files of the correct
size. A missing or inaccessible icon can block PWA installation
on iOS.
Service Worker: why you cannot cache authentication and API
A common mistake is aggressive caching that breaks authentication.
Here's the approach from Kazki AI: we cache only static assets, everything dynamic
— we skip entirely:
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// Do not cache: API, authentication, dynamic pages
if (event.request.method !== 'GET') return;
if (url.pathname.startsWith('/api/')) return;
if (url.pathname.startsWith('/login') ||
url.pathname.startsWith('/logout') ||
url.pathname.startsWith('/oauth2')) return;
if (url.pathname.startsWith('/dashboard') ||
url.pathname.startsWith('/profile')) return;
// For the rest — network-first, fallback to cache
event.respondWith(
fetch(event.request)
.then(response => {
if (response.ok) {
const clone = response.clone();
caches.open(CACHE_NAME)
.then(cache => cache.put(event.request, clone));
}
return response;
})
.catch(() => caches.match(event.request))
);
});
If the authentication page is cached — the user will see an outdated
version or "get stuck" in a cached state after logging out.
Learn more about common caching errors in PWA — including aggressive
HTML caching and routing issues — in the article
8 critical errors when integrating PWA
.
💼 Section 6. Push handler: try/catch fallback and iOS specifics from a real project
iOS sometimes sends a push event with data
not in JSON format — without try/catch, the Service Worker will crash and the subscription
will be automatically canceled after several such failures.
Apple documents that if a Service Worker does not display a notification
after receiving a push event — iOS revokes notification permission
for that site. Therefore, event.waitUntil and a correct
fallback are critically important.
[Apple Developer: Sending Web Push Notifications]
If the Service Worker does not display a notification after a push event —
iOS treats it as a "silent push" and cancels the subscription.
[dev.to: iOS push subscriptions terminated after 3 notifications]
Full push handler from Kazki AI with explanations:
self.addEventListener('push', event => {
// try/catch — iOS sometimes sends non-JSON payload
let data = {};
try {
data = event.data?.json() ?? {};
} catch (e) {
data = { title: 'Kazki AI', body: event.data?.text() ?? '' };
}
// Badging API — show counter on icon
const badgePromise = self.navigator?.setAppBadge
? self.navigator.setAppBadge(data.badgeCount || 1)
: Promise.resolve();
// event.waitUntil — MANDATORY, otherwise iOS will cancel subscription
event.waitUntil(
Promise.all([
self.registration.showNotification(data.title || 'Kazki AI', {
body: data.body || '',
icon: '/images/favicon.jpg',
badge: '/images/favicon.jpg',
data: { url: data.url || '/dashboard' }
}),
badgePromise
])
);
});
// Click on notification — open the required page
self.addEventListener('notificationclick', event => {
event.notification.close();
if (self.navigator?.clearAppBadge) {
self.navigator.clearAppBadge();
}
// Open URL from payload or default /dashboard
event.waitUntil(
clients.openWindow(event.notification.data.url)
);
});
Payload structure sent by the backend (Spring Boot + nl.martijndwars/web-push):
{
"title": "New story is ready!",
"body": "Your story about the dragon is already waiting",
"icon": "/images/favicon.jpg",
"url": "/library",
"badgeCount": 1
}
💼 Section 7. 7 reasons why pushes don't arrive on iPhone — and how to fix it
Most problems with iOS PWA Push
are related not to the server, but to how client-side code interacts
with Safari's limitations.
Problems with push subscriptions on iOS are one of the most discussed
topics on Apple Developer Forums. Below are seven of the most common reasons
with specific solutions.
[Apple Developer Forums: PWA Push Issues]
1. PWA not installed on the home screen
The most common reason. The Push API is simply unavailable until the app
is opened in standalone mode. Solution: add a UI hint
with instructions "Share → Add to Home Screen".
2. Service Worker does not display a notification after a push event
iOS treats a push without showNotification as "silent"
and cancels the subscription after several such occurrences. Solution:
always call showNotification inside
event.waitUntil.
[dev.to: iOS push terminated after 3 notifications]
3. VAPID subject in incorrect format
Apple requires the VAPID subject to be either a mailto: address,
or a full HTTPS URL. Any other format returns
403 Forbidden from web.push.apple.com.
Solution: check vapid.subject in the server configuration.
[Longsight: Implementing Cross-Platform Push]
4. Subscription "disappeared" after re-authentication
If the frontend code calls subscription.unsubscribe()
during logout or reauthentication — Safari will not allow
subscribing again without an explicit user gesture. Solution: do not
cancel the subscription on logout, only deactivate it on the server.
[XenForo: Lost push subscriptions iOS PWA]
5. HTTP instead of HTTPS
Service Worker and Push API require HTTPS without exception.
Even a redirect from HTTP to HTTPS can interrupt SW registration.
Solution: ensure that the entire site, including static assets,
is served over HTTPS.
6. Permission request not called from user gesture
Notification.requestPermission() called in
setTimeout, DOMContentLoaded or
any automatic code — will be silently blocked on iOS.
Solution: call it exclusively within a button click handler.
[pwa.io: Web Push iOS Safari 16.4]
7. Endpoint returned 410 or 404 — subscription not removed from DB
When Apple invalidates a subscription — the server receives a status
410 Gone or 404. If this endpoint is not removed
from the database — subsequent attempts to send a push to this
user will waste resources. Solution:
handle these status codes and automatically remove the subscription.
// Spring Boot: handling invalid subscriptions
if (statusCode == 410 || statusCode == 404 || statusCode == 400) {
pushSubscriptionRepository.deleteByEndpoint(sub.getEndpoint());
log.info("Removed invalid subscription: {}", sub.getEndpoint());
}
💼 Section 8. Workarounds and fallback strategies
If PWA Push on iOS is not suitable for
your use case — email and in-app notifications remain a more reliable
alternative with a higher delivery rate.
PWA Push on iOS works, but has limitations: installation is required,
possible loss of subscriptions, lower delivery rate compared to native.
For critical notifications, it's worth having a fallback strategy.
A reliable notification system is not a choice between Push and email,
but a combination of both with graceful degradation.
Email as the primary fallback
Email has a delivery rate of about 90–95% and does not depend on
the iOS version or PWA installation status. For transactional
notifications (payment, confirmation, important events), email
remains a more reliable channel.
In-app notifications
An unread counter in the header, an event feed inside
the app — this is what the user is guaranteed to see on the next
PWA opening, regardless of the push subscription status.
Combined approach with Kazki AI
Kazki AI uses a three-tier system:
- Push notifications — for installed PWAs on iOS and Android
- Email — for all users as a fallback
- Badge on the icon (Badging API) — for installed PWAs
This allows reaching 100% of the audience regardless of whether
the user has installed the PWA on the home screen.
Learn more about choosing between PWA and a native app —
PWA vs Native in 2026: comparison and a real case
.
❓ Frequently Asked Questions (FAQ)
Does Web Push work on iOS without PWA installation?
No. The Push API on iOS is exclusively available for Home Screen web apps —
i.e., websites added via Safari → Share → Add to Home Screen.
An open tab in Safari or any other browser does not have
access to PushManager.
[WebKit Blog: Web Push for Web Apps on iOS]
Does PWA Push work via Chrome on iPhone?
No. Chrome, Firefox, and any other browser on iOS use
WebKit as their engine — this is an Apple requirement. Therefore, all browsers on iOS have
the same limitations regarding the Push API. Push only works via Safari
provided the PWA is installed on the home screen.
[Apple Developer Forums: Push in Chrome iOS]
From which iOS version is Web Push supported for PWA?
From iOS 16.4 (March 2023). Versions prior to 16.4 do not support
Web Push for PWA regardless of browser or settings.
According to StatCounter, as of early 2026, over
95% of iPhones worldwide run on iOS 16 or newer versions.
[WebKit Blog: Web Push iOS 16.4]
Why does the subscription disappear after a few days?
There are several reasons. First — iOS cancels the subscription if the Service Worker
received push events but did not display a notification. Second — if
the PWA has not been opened for a long time, Safari may clear the Service Worker's state.
Solution: always call showNotification in
event.waitUntil and check the subscription's relevance
every time the app is opened.
[dev.to: iOS push subscriptions terminated]
What is the delivery rate of PWA Push on iOS compared to Android?
Based on the experience of teams that have publicly shared data — the delivery rate
on iOS is approximately 70–85% compared to 90–95% on Android.
The difference is due to additional Safari limitations: stricter
background process management, possible loss of subscription after
prolonged inactivity.
[OneSignal: iOS Web Push delivery]
Can a push be sent without a backend library?
Technically yes — the Web Push Protocol is an open standard. But
self-implementing VAPID signing and payload encryption is complex
and prone to errors. For Java/Spring Boot, the
library nl.martijndwars:web-push, is recommended for Node.js — web-push.
<!-- Maven: dependency for Java/Spring Boot -->
<dependency>
<groupId>nl.martijndwars</groupId>
<artifactId>web-push</artifactId>
<version>5.1.1</version>
</dependency>
How to generate VAPID keys?
The easiest way — via web-push CLI:
npx web-push generate-vapid-keys
The public key is used on the frontend for subscription,
the private key — on the backend for signing each push request.
Never publish the private key in a public repository.
✅ Conclusions: when PWA Push on iOS is sufficient, and when native is better
PWA Push on iOS works and is suitable for
most content and SaaS products. For critical notifications
in fintech, medicine, or e-commerce with a large share of iOS audience,
it is worth either combining with email or considering native.
With iOS 16.4, the technical barrier has been removed. What remains is operational:
the user must install the PWA on the home screen — and this is the main
limitation that cannot be solved at the code level.
PWA Push on iOS is not an "either it works, or it doesn't" situation.
It's a tool with specific operating conditions. If these conditions are met —
it works stably.
PWA Push on iOS is suitable if:
- Your product is content-based: news, media, education, SaaS
- The audience is loyal and willing to install the PWA on the home screen
- Notifications are informational, not critical (reminders, news, updates)
- Email is available as a fallback for those who haven't installed the PWA
- The budget does not allow for parallel native app development
Consider native or hybrid if:
- Notifications are critical: banking transactions, medical alerts
- The share of iOS in your audience exceeds 60%
- Background processes or real-time geolocation are required
- App Store presence is part of the marketing strategy
Conclusion from Kazki AI's experience
In Kazki AI, PWA Push on iOS works satisfactorily for informational
notifications — "new story is ready", "subscription reminder".
The delivery rate is lower than on Android, but acceptable for a product
where delivery criticality is not high. Email remains the primary
channel for transactional notifications.
If you are still choosing between PWA and a native app and haven't decided
on the architecture — a detailed comparison with real figures is in the article
PWA vs Native in 2026: comparison and a real case
.
📖 Sources