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.
- 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
| Piece | Runs in | Job |
|---|---|---|
| Page script | Your web page | Registers the worker, asks permission, subscribes |
Service worker (/sw.js) | Background, browser-managed | Receives push events, shows notifications |
| Your server | Backend | Stores subscriptions, signs and encrypts messages |
| Push service | Browser vendor | Delivers 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
| State | What to show |
|---|---|
| Unsupported (no PushManager) | Hide the button, or offer email |
default | Your “Get notifications” button |
granted + subscription | “Notifications on” with a turn-off option |
granted, no subscription | Silently re-subscribe (e.g. after a reset) |
denied | Short instructions to re-enable in site settings — you can’t prompt again |
| iPhone in a Safari tab | Add 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.
Keep reading
Service worker push events, done right
push and notificationclick handlers, focusing open tabs, action buttons, subscription changes and pitfalls.
VAPID keys for web push, explained
What the key pair does, how the signed JWT works, how to generate keys and why rotation breaks lists.
Web push notifications with Node.js
Install web-push, generate keys, save subscriptions with Express, send with TTL and clean up dead endpoints.
The Web Push protocol, explained
RFC 8030 delivery headers, RFC 8291 encryption, RFC 8292 VAPID, status codes and payload size.
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.