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.
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.
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 · All guides
Free for life. No limits.
Unlimited subscribers, campaigns and automations. No badges. About five minutes to install.