Skip to content

Web Push: subscriptions, permissions, and delivery

In one line: Web Push pairs the Push API (a service-worker subscription tied to a browser push service) with the Notifications API (showing a message). Your server encrypts a payload, POSTs it to the subscription endpoint, the push service wakes your service worker’s push event, and you call self.registration.showNotification(). On iOS/Safari, this only works for a PWA the user has installed to the Home Screen.

  1. Subscribe. With an active service worker registration, call registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey }), passing your VAPID public key. The browser returns a PushSubscription containing an endpoint URL plus p256dh and auth keys.
  2. Store. Send the subscription JSON to your server and persist it per user/device.
  3. Send. Your server encrypts the payload (RFC 8291), signs a VAPID JWT (RFC 8292), and POSTs to the subscription’s endpoint — the push service operated by the browser vendor.
  4. Receive. The push service delivers to the device, which wakes the service worker and fires a push event even when no page is open.
  5. Show. In the push handler you call showNotification(); a click fires notificationclick, where you focus or open the right window.

Notification.requestPermission() (or the implicit prompt during subscribe()) must be triggered by a user gesture — a click or tap, not on page load. Ask in context, after the user does something that explains why notifications help. Permission has three states:

  • default — not yet decided; you may prompt once, tactfully.
  • granted — you may subscribe and show notifications.
  • denied — terminal in practice; you cannot re-prompt, so never spam the request.

Because userVisibleOnly: true is required, every push you send must surface a visible notification — silent background push is not permitted.

Handling push and clicks in the service worker

Section titled “Handling push and clicks in the service worker”
self.addEventListener('push', (event) => {
const data = event.data?.json() ?? {};
event.waitUntil(
self.registration.showNotification(data.title ?? 'Update', {
body: data.body,
icon: '/icons/icon-192.png',
data: { url: data.url ?? '/' },
})
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(clients.openWindow(event.notification.data.url));
});

Wrap async work in event.waitUntil() so the service worker stays alive until the notification is shown or the click is handled.

Browser / PlatformSupportSinceConfidenceSourceNotes
Chrome (Android)✅ yes50highref
Chrome (Desktop)✅ yes50highref
Edge (Desktop)✅ yes17highref
Safari (iOS)⚠️ partial16.4mediumrefOnly for home-screen-installed web apps; requires user-gesture permission and Web Push via APNs.
Safari (macOS)✅ yes16mediumref
Firefox (Desktop)✅ yes44highref
Samsung Internet✅ yes5.0highref

Ecosystem & commercial policy

EntityTypeContextStatusSponsoredNotes
Apple Push (APNs)delivery_policyiOS⚠️ partialNoiOS Web Push requires the user to add the app to the Home Screen first.

Source: spec · MDN · Last verified 2026-06-24 · Confidence: high

Decision question Recommended action Rationale
When do I request permission? After a user gesture that gives context. Browsers ignore non-gesture prompts; cold prompts get denied.
What auth do I use to send? VAPID (application server keys). Identifies your server to the push service; no per-vendor accounts needed.
Do I need an encrypted payload? Yes, for any payload (RFC 8291). Push services only relay ciphertext; the browser decrypts with p256dh/auth.
Need to reach iOS users? Require Home Screen install first. iOS/Safari only delivers web push to installed PWAs.
A send returns 404/410? Delete that subscription. The endpoint is gone or expired; stop sending to it.
  • Register a service worker and confirm it is active before subscribing.
  • Request permission from a real user gesture, in context — never on load.
  • Subscribe with userVisibleOnly: true and your VAPID applicationServerKey.
  • Persist the full PushSubscription (endpoint + p256dh + auth) server-side.
  • Encrypt payloads (RFC 8291) and sign VAPID JWTs (RFC 8292) when sending.
  • Always call showNotification() in the push handler (required by userVisibleOnly).
  • Handle notificationclick to focus an existing client or open the target URL.
  • Prune subscriptions on 404/410 responses from the push service.
  • For iOS/Safari, gate the prompt behind an installed (Home Screen) PWA.