Add push notifications
Published
At the end of this guide your server can wake the user’s device with a message while your
app is closed: the page subscribes its service worker to the browser’s push service, your
server posts to the subscription endpoint, and the worker turns the push event into a
system notification. Two APIs are involved. Push is the transport that delivers the
message to the worker; the Notifications API is the display the user sees.
You need an active service worker (follow Getting started if you have none), an HTTPS origin, and a VAPID key pair on the server. Support position: the Push API has been available across browsers since 2023-03; Safari delivers it to Home Screen web apps only, from iOS and iPadOS 16.4, and to Safari 16.1 on macOS Ventura. The per-browser rows are in Web Push.
1. Confirm support before offering the button
Section titled “1. Confirm support before offering the button”ServiceWorkerRegistration.pushManager is the entry point; PushManager is absent on
window where the browser has no push. Hide the subscribe control in that case and keep
whatever in-app channel you already have (an inbox view, an email digest). On iOS the
check also fails in a Safari tab and passes only once the user has added the app to the
Home Screen, so the fallback copy should say so.
export function pushSupported() { return 'serviceWorker' in navigator && 'PushManager' in window && 'Notification' in window;}2. Request permission from a click
Section titled “2. Request permission from a click”Notification.requestPermission() resolves to granted, denied, or default; treat
default as a denial. Call it inside the click handler of a button whose label says what
the notifications are for. Firefox 72 refuses requests that are not triggered by a user
gesture, Safari requires the same direct interaction, and a denied answer is final until
the user changes it in browser settings, so one unprompted request on page load can cost
you the channel for good.
subscribeButton.addEventListener('click', async () => { const permission = await Notification.requestPermission(); if (permission !== 'granted') { subscribeButton.disabled = true; subscribeButton.textContent = 'Notifications are off for this site'; return; } await subscribe();});3. Subscribe with your VAPID public key
Section titled “3. Subscribe with your VAPID public key”Still inside the gesture, call pushManager.subscribe() with userVisibleOnly: true and
the server’s VAPID public key as applicationServerKey. Chrome and Edge reject the promise
when userVisibleOnly is not true, and they require the key (or a legacy
gcm_sender_id in the manifest). The key is an ECDSA P-256 public key, Base64url-encoded
on the server; it is not the ECDH key that encrypts payloads.
function base64UrlToUint8Array(base64Url) { const padding = '='.repeat((4 - (base64Url.length % 4)) % 4); const base64 = (base64Url + padding).replace(/-/g, '+').replace(/_/g, '/'); return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));}
async function subscribe() { const registration = await navigator.serviceWorker.ready; const existing = await registration.pushManager.getSubscription(); const subscription = existing || (await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: base64UrlToUint8Array(VAPID_PUBLIC_KEY), })); await fetch('/api/push/subscribe', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(subscription.toJSON()), });}4. Store the subscription and treat it as expiring
Section titled “4. Store the subscription and treat it as expiring”subscription.toJSON() yields the endpoint plus the p256dh and auth keys the server
needs to encrypt and send. The endpoint is a capability URL: anyone who holds it can push
to that user, so store it server-side only and protect the subscribe route against CSRF.
Each subscription belongs to one service worker registration, and the push service may set
an expiry, in which case the worker receives pushsubscriptionchange; re-subscribe there
and update the server.
self.addEventListener('pushsubscriptionchange', (event) => { event.waitUntil( self.registration.pushManager .subscribe(event.oldSubscription.options) .then((subscription) => fetch('/api/push/subscribe', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(subscription.toJSON()), }), ), );});5. Show the notification in the worker
Section titled “5. Show the notification in the worker”The push event carries the decrypted payload in event.data. Because the subscription
promised user-visible messages, show a notification for each push; Chrome has no delivery
quota, while Firefox limits pushes that do not produce a notification and refreshes the
quota on each visit. Handle notificationclick to focus an open window or open a new one.
self.addEventListener('push', (event) => { const data = event.data ? event.data.json() : { title: 'New message', body: '' }; event.waitUntil( self.registration.showNotification(data.title, { body: data.body, icon: '/icons/icon-192.png', data: { url: data.url || '/' }, }), );});
self.addEventListener('notificationclick', (event) => { event.notification.close(); event.waitUntil( self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clients) => { const open = clients.find((client) => 'focus' in client); return open ? open.focus() : self.clients.openWindow(event.notification.data.url); }), );});6. Send a test push from DevTools
Section titled “6. Send a test push from DevTools”Before writing server code, open Chrome DevTools, go to Application > Service
workers, type a payload into the field next to the Push button and click Push.
The worker’s push handler runs with that text; event.data.json() throws on plain text,
so test with {"title":"Hi","body":"test"}. A notification appears from the operating
system with your icon, and clicking it focuses the app. Then send the same payload from
the server with a Web Push library that signs a VAPID JWT and encrypts with the stored
keys.
See also
Section titled “See also”- Web Push: subscriptions, permissions, and delivery
- The Notifications API: system notifications
- The notification permission model
- iOS and Safari Web Push (16.4+)
- PushManager: subscribe() method (developer.mozilla.org)
- Web Push for Web Apps on iOS and iPadOS (webkit.org)
- Notification: requestPermission() static method (developer.mozilla.org)
← Back to the Guides overview.