Safari Web Push: How to Ship Push Notifications to macOS and iOS
Safari now supports the standard Web Push API on macOS Ventura and iOS 16.4+. Learn how to integrate with APNs, deploy a single push stack, and navigate iOS installation constraints with a practical example and engineering trade‑offs.
12 Sept 2026, 14:33 UTC

Why Safari Web Push Matters
Until 2022, Safari was the only major browser that didn’t support the web‑wide Push API. That meant a web team that wanted to reach macOS or iOS users had to ship a native app or rely on the legacy safari.pushNotification API, which required an Apple Developer account and APNs certificates. With Safari 16 on macOS Ventura and iOS 16.4+, Apple finally opened the door to the standard Push API, letting developers use a single implementation across Chrome, Firefox, Edge, and Safari.
When Safari Added Push Support
- macOS Ventura (Safari 16, 2022) – full Push API, Service Worker, Notification API.
- iOS 16.4+ (iPadOS 16.4+) – same API, but only works for sites installed to the Home Screen.
Because the same JavaScript interfaces are used, you can ship one push stack that targets all three platforms. The only extra caveat is the iOS installation requirement, which becomes a key design decision.
How Safari Routes Push Through APNs
Safari does not expose APNs directly to the web page. The browser itself talks to APNs behind the scenes. From the developer’s point of view you only:
- Generate a VAPID key pair (public/private).
- Use
applicationServerKeywhen callingpushManager.subscribe(). - Send the resulting
PushSubscriptionJSON to your server. - Let a Web Push library (e.g.,
web-pushfor Node) encrypt and forward the payload to APNs.
No APNs certificates or Apple Developer account are required for the standards‑based flow.
Implementing Push in Safari
1. Register a Service Worker
// register-sw.js
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').then(reg => {
console.log('SW registered', reg);
});
}
2. Request Permission and Subscribe
// In a user‑gesture handler
Notification.requestPermission().then(permission => {
if (permission !== 'granted') return;
navigator.serviceWorker.ready.then(reg => {
reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array('')
}).then(sub => {
// POST sub to your server
fetch('/api/save-subscription', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(sub)
});
});
});
});
Replace <YOUR_VAPID_PUBLIC_KEY> with the base64 string of the public key you generated. The urlBase64ToUint8Array helper is the same one used in the Web Push spec examples.
3. Service Worker Push Listener
// sw.js
self.addEventListener('push', event => {
const data = event.data ? event.data.json() : {};
const title = data.title || 'New message';
const options = {
body: data.body,
icon: data.icon,
badge: data.badge
};
event.waitUntil(self.registration.showNotification(title, options));
});
4. Send a Test Message from the Server
const webPush = require('web-push');
webPush.setVapidDetails(
'mailto:[contact removed]',
'',
''
);
const payload = JSON.stringify({ title: 'Hello', body: 'Safari push test' });
const subscription = /* fetch from DB */;
webPush.sendNotification(subscription, payload)
.then(() => console.log('Sent'))
.catch(err => console.error(err));
When the payload arrives, Safari will decrypt it and display the notification.
Practical Example: End‑to‑End Flow
- Open
https://example.comon macOS Safari 16+. In the console run the registration code above. - Permission dialog appears, grant it.
- Service worker registers, subscription is posted to
/api/save-subscription. - Server uses
web-pushto send a payload to the stored subscription. - Notification shows on the desktop banner.
- Repeat the same steps on an iPhone running iOS 16.4+ after adding the site to the Home Screen. The notification appears in the same way.
To verify, check the browser console for the subscription JSON, and confirm that the notification surface appears on both platforms. On iOS, make sure the web app is launched from the Home Screen; otherwise push won’t be delivered.
Engineering Trade‑offs
- Installation Requirement on iOS: Only Home‑Screen‑installed web apps can receive push. This limits the potential audience and reduces opt‑in rates compared to desktop. Teams should tie the permission prompt to a clear value moment (e.g., a user sets up a new account or schedules a reminder) and offer an alternative channel (email, in‑app inbox) if the user declines.
- APNs Throttling: APNs may throttle or drop messages if you send too many at once. Use exponential back‑off or batching on the server side.
- VAPID Key Management: Hand‑rolling encryption is error‑prone. Prefer a maintained library and keep the private key secure.
- Legacy API Compatibility: If you need to support macOS Safari versions older than 16, you must still ship a
safari.pushNotificationfallback. For most new projects, drop the legacy path.
Next Steps
- Generate a VAPID key pair and store the private key securely.
- Update your site to register a service worker and request push permission in a user‑gesture context.
- Implement the push listener in the service worker.
- Deploy a server endpoint to receive subscriptions and integrate a Web Push library.
- Test on macOS Safari 16+ and iOS 16.4+ (Home‑Screen‑installed). Verify that notifications appear and that the user can revoke permission from Settings.
- Plan fallback channels for users who opt out or for older Safari browsers.
With these steps, you can ship a single push stack that reaches both desktop and mobile Safari users, leveraging the same code you already use for Chrome or Firefox.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.