Skip to content
Developers · 10 min read

The Web Push API in JavaScript: a working example

Browser push takes four pieces of code: register a service worker, ask permission after a click, subscribe with your VAPID public key, and send the subscription to your server. Here they are, end to end.

Updated By the Pusharoo team
In short
  • navigator.serviceWorker.register() installs the worker that will receive pushes.
  • Notification.requestPermission() must run in response to a click in Firefox and Safari.
  • registration.pushManager.subscribe() needs userVisibleOnly: true and your VAPID public key.
  • Send the resulting PushSubscription JSON to your server; it’s the address you push to.

The moving parts

PieceRuns inJob
Page scriptYour web pageRegisters the worker, asks permission, subscribes
Service worker (/sw.js)Background, browser-managedReceives push events, shows notifications
Your serverBackendStores subscriptions, signs and encrypts messages
Push serviceBrowser vendorDelivers to the device

Conceptual overview: how web push works.

1. Register the service worker

if ('serviceWorker' in navigator && 'PushManager' in window) { const registration = await navigator.serviceWorker.register('/sw.js', { scope: '/', updateViaCache: 'none' // always check for a fresh sw.js }); }

Serve sw.js from your root so its scope covers the whole site. A worker at /js/sw.js only controls /js/ unless you send a Service-Worker-Allowed header.

2. Ask permission after a click

document.querySelector('#notify-btn').addEventListener('click', async () => { const permission = await Notification.requestPermission(); if (permission !== 'granted') return; // 'denied' or 'default' await subscribe(); });

Firefox and Safari ignore permission requests that don’t follow a user gesture, and Chrome may quiet sites that ask on load. Show your own explanation first (two-step opt-in).

3. Subscribe with your VAPID public key

function urlBase64ToUint8Array(base64) { const pad = '='.repeat((4 - base64.length % 4) % 4); const raw = atob((base64 + pad).replace(/-/g, '+').replace(/_/g, '/')); return Uint8Array.from([...raw].map(c => c.charCodeAt(0))); } async function subscribe() { const registration = await navigator.serviceWorker.ready; let sub = await registration.pushManager.getSubscription(); if (!sub) { sub = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY) }); } await fetch('/api/push/subscriptions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(sub) // { endpoint, expirationTime, keys: { p256dh, auth } } }); }

The public key is safe to ship in your page; the private key must stay on the server (VAPID keys).

4. Show the notification in the worker

// sw.js self.addEventListener('push', event => { const data = event.data ? event.data.json() : {}; event.waitUntil(self.registration.showNotification(data.title || 'Update', { body: data.body, icon: data.icon || '/icon-192.png', data: { url: data.url || '/' } })); }); self.addEventListener('notificationclick', event => { event.notification.close(); event.waitUntil(clients.openWindow(event.notification.data.url)); });

More patterns — focusing an open tab, action buttons, subscription changes — in service worker push events.

5. Send from your server

Sending means encrypting the payload for that subscription’s keys, signing a VAPID JWT and POSTing to the endpoint. Don’t hand-roll the crypto; use a maintained library: web-push for Node.js, minishlink/web-push for PHP, or pywebpush for Python. Protocol details: the Web Push protocol.

Handle every permission state

StateWhat to show
Unsupported (no PushManager)Hide the button, or offer email
defaultYour “Get notifications” button
granted + subscription“Notifications on” with a turn-off option
granted, no subscriptionSilently re-subscribe (e.g. after a reset)
deniedShort instructions to re-enable in site settings — you can’t prompt again
iPhone in a Safari tabAdd to Home Screen instructions
const isiOS = /iPhone|iPad/.test(navigator.userAgent); const standalone = matchMedia('(display-mode: standalone)').matches || navigator.standalone; if (isiOS && !standalone) showHomeScreenHint();

Keep subscriptions fresh

Browsers occasionally rotate subscriptions, and users clear site data. On each visit (or once a day), call pushManager.getSubscription() and upsert it on your server. If permission is granted but there’s no subscription, subscribe again silently. If the stored key doesn’t match your current VAPID key — for example after you rotated keys — unsubscribe and resubscribe with the new one.

Let people turn it off

async function turnOff() { const reg = await navigator.serviceWorker.ready; const sub = await reg.pushManager.getSubscription(); if (sub) { await fetch('/push/unsubscribe', { method: 'POST', body: JSON.stringify({ endpoint: sub.endpoint }) }); await sub.unsubscribe(); } }

An in-page off switch is kinder than sending people to browser settings, and it keeps your list honest.

Or skip the plumbing

If you’d rather not run subscription storage, a sending queue, 404/410 cleanup, a soft prompt and an iPhone Home Screen flow yourself, Pusharoo does all of it for free: upload sw.js, add one script tag, and use Pusharoo.subscribe() from your own button (JavaScript API).

Questions

Do I need Firebase to use the Push API?

No. The Push API works with standard VAPID keys in every major browser. Firebase is optional (comparison).

Why does pushManager.subscribe() fail?

Common causes: permission not granted, page not on HTTPS, an invalid applicationServerKey, or (on iPhone) the page isn’t running as a Home Screen web app.

Can I send a push from JavaScript in the browser?

No — sending requires your VAPID private key, which must never be exposed in the browser. Send from a server.

What is applicationServerKey?

It’s your VAPID public key, passed to pushManager.subscribe(). Browsers accept a base64url string or a Uint8Array. The push service then only accepts messages signed with the matching private key.

Why is userVisibleOnly required?

Chrome requires userVisibleOnly: true, a promise that each push shows a notification. Silent background pushes aren’t allowed for websites.

More in Developers: Web push notifications in PHP · Laravel browser push notifications (no Firebase) · Web push notifications in React and Next.js · Declarative Web Push, explained · Firebase web push vs. standard web push · Web push in Python with pywebpush · Django push notifications to the browser · Web push notifications in Ruby on Rails · Web push in Go with webpush-go · Angular push notifications with SwPush · Push notifications in Vue 3 and Nuxt · Push notifications in SvelteKit · All guides

Free for life. No limits.

Unlimited subscribers, campaigns and automations. No badges. About five minutes to install.