The essentials

Quick reference

One focused task per row. Jump to the related section for complete, working examples.

UseSyntaxExamples
Register a service workerconst registration = await navigator.serviceWorker.register('/sw.js', { scope: '/' })View examples
Wait for an active workerconst registration = await navigator.serviceWorker.readyView examples
Request notification permissionconst permission = await Notification.requestPermission()View examples
Read current permissionif (Notification.permission === 'granted') enablePush()View examples
Create a push subscriptionawait registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey })View examples
Reuse a subscriptionconst subscription = await registration.pushManager.getSubscription()View examples
Unsubscribe locallyawait subscription.unsubscribe()View examples
Extend a push eventevent.waitUntil(handlePush(event))View examples
Show a notificationawait self.registration.showNotification(title, options)View examples
Replace related notifications{ tag: 'inbox', renotify: false }View examples
Handle a notification clickself.addEventListener('notificationclick', event => event.waitUntil(openApp(event)))View examples
Find application windowsconst windows = await clients.matchAll({ type: 'window', includeUncontrolled: true })View examples
Set an application badgeawait navigator.setAppBadge(unreadCount)View examples
Clear the application badgeawait navigator.clearAppBadge()View examples

Web push delivers server-originated messages to a service worker, which can display notifications while no page is open. The browser push service, application server, service worker, Notifications API, and Badging API have distinct responsibilities. Ask permission only after informed user intent, store subscriptions like credentials, encrypt payloads using established libraries, and make every event handler finish through waitUntil().

Step by step

Detailed examples

01

Install one service worker before enabling push

PushManager belongs to a ServiceWorkerRegistration and secure contexts are required. Register a stable, same-origin worker URL with the narrowest useful scope, then await navigator.serviceWorker.ready before subscribing. Worker upgrades are asynchronous and old clients can coexist with new code, so version messages and storage formats compatibly. Push is not a guaranteed or real-time transport; the user agent or push service may delay, coalesce, expire, or discard messages.

Feature-detect and obtain the active registration
async function preparePush() {
  if (!isSecureContext || !('serviceWorker' in navigator) || !('PushManager' in window)) return null;
  await navigator.serviceWorker.register('/sw.js', { scope: '/' });
  return navigator.serviceWorker.ready;
}
Back to quick reference ↑
02

Ask only after explaining value and user control

Notification permission has granted, denied, and default states; default must be treated as not granted. Browsers may require transient user activation and may suppress abusive prompts. Explain the notification category and frequency in application UI, then call requestPermission() from the user's action. Never repeatedly prompt after denial, never gate unrelated functionality, and provide settings to disable categories and unsubscribe. Permission can later be revoked outside the page.

Request from an explicit enable button
enableButton.addEventListener('click', async () => {
  if (!('Notification' in window)) return showUnsupported();
  const permission = Notification.permission === 'default'
    ? await Notification.requestPermission()
    : Notification.permission;
  if (permission !== 'granted') return showPushDisabled();
  await enablePushSubscription();
});
Back to quick reference ↑
03

Bind subscriptions to accounts and protect endpoints

subscribe() normally uses userVisibleOnly true and an applicationServerKey containing the VAPID P-256 public key bytes, not its base64url text. A PushSubscription JSON representation contains a capability URL plus encryption material; anyone holding the endpoint may consume quota or attempt delivery, so transmit it over authenticated HTTPS, apply CSRF protection, bind it to the correct account and device, and never log it. On logout, delete the server binding and unsubscribe where product policy requires it.

Create once and register with the server
async function enablePushSubscription() {
  const registration = await navigator.serviceWorker.ready;
  const existing = await registration.pushManager.getSubscription();
  const subscription = existing ?? await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: base64urlToBytes(VAPID_PUBLIC_KEY),
  });
  const response = await fetch('/api/push/subscriptions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(subscription) });
  if (!response.ok) throw new Error('Subscription registration failed');
}
Back to quick reference ↑
04

Validate payloads and show useful persistent notifications

A push event may have no data. Parse untrusted payloads defensively, enforce a version and size limit, and avoid placing secrets in notification text or URLs. Use a maintained Web Push library on the server to implement payload encryption and VAPID authorization; Web Crypto calls alone do not implement the protocol. waitUntil() must cover parsing, data refresh, and showNotification(). In normal userVisibleOnly deployments, silent background work is not a substitute for a user-visible notification.

Handle a versioned payload with a safe fallback
self.addEventListener('push', event => {
  event.waitUntil((async () => {
    let message = { title: 'Updates available', body: 'Open the app to refresh.' };
    try {
      const value = event.data?.json();
      if (value?.version === 1 && typeof value.title === 'string') message = value;
    } catch {}
    await self.registration.showNotification(message.title, { body: message.body, tag: message.tag, data: { path: safePath(message.path) } });
  })());
});
Back to quick reference ↑
05

Route clicks to same-origin destinations

notificationclick runs in the service worker. Close the notification when appropriate, validate action identifiers and stored routes against an allowlist, then focus a suitable existing same-origin WindowClient or open a new one. clients.openWindow() may be restricted to user activation supplied by the click. Do not accept an arbitrary payload URL, since that can create an open redirect or phishing surface. Handle notificationclose separately if the product truly needs coarse dismissal analytics.

Focus an existing window or open a safe route
self.addEventListener('notificationclick', event => {
  event.notification.close();
  event.waitUntil((async () => {
    const path = safePath(event.notification.data?.path) || '/inbox';
    const windows = await clients.matchAll({ type: 'window', includeUncontrolled: true });
    const existing = windows.find(client => new URL(client.url).origin === self.location.origin);
    if (existing) { await existing.navigate(path); return existing.focus(); }
    return clients.openWindow(path);
  })());
});
Back to quick reference ↑
06

Use badges as optional, reconstructible hints

The Badging API sets a number or flag associated with an installed app on supporting operating systems. Window and service-worker globals expose different navigator objects but can both provide setAppBadge and clearAppBadge. Support, installation requirements, display, limits, and permission behavior vary; feature-detect and never make the badge the sole unread indicator. Keep the server or durable app state authoritative because a badge can outlive a page and can be cleared by the platform.

Update a badge without breaking unsupported clients
async function updateBadge(count) {
  if (!('setAppBadge' in navigator)) return;
  try {
    if (count > 0) await navigator.setAppBadge(Math.floor(count));
    else await navigator.clearAppBadge();
  } catch (error) {
    console.debug('Badge unavailable', error.name);
  }
}
Back to quick reference ↑

Sources and further reading

References

Authoritative documentation used to verify and expand this cheat sheet.

  1. World Wide Web ConsortiumPush APIw3.org
  2. World Wide Web ConsortiumNotifications API Standardnotifications.spec.whatwg.org
  3. World Wide Web ConsortiumService Workers Nightlyw3.org
  4. World Wide Web ConsortiumBadging APIw3.org
  5. Internet Engineering Task ForceGeneric Event Delivery Using HTTP Pushrfc-editor.org
  6. Internet Engineering Task ForceVoluntary Application Server Identification for Web Pushrfc-editor.org

Help us improve

Found a typo or missing example?

Tell us what would make this cheat sheet clearer, more complete, or more useful.

Share feedback