How Web Push Actually Works: Service Workers, VAPID and FCM in Plain Terms
Web push looks simple from the outside. A visitor clicks "Allow", and later a notification appears even though your site is closed. Underneath, there are four moving parts, and once you understand how they fit together,
Web push looks simple from the outside. A visitor clicks "Allow", and later a notification appears even though your site is closed. Underneath, there are four moving parts, and once you understand how they fit together, most push bugs become easy to diagnose.
This is a developer-friendly walkthrough of the whole flow, with no vendor SDK in the way.
The four parts
- Your site, which asks for permission and creates the subscription.
- A service worker, a script the browser keeps registered for your origin that can wake up when a message arrives.
- A push service, run by the browser vendor. Chrome and most Chromium browsers use Firebase Cloud Messaging; Firefox uses Mozilla's autopush; Safari uses Apple's service.
- Your application server, which stores subscriptions and sends messages to the push service.
MDN's Push API reference is the best single page for the browser side of this.
Step 1: register a service worker
Everything starts with a service worker served from your own domain, usually at the root so its scope covers the whole site:
const reg = await navigator.serviceWorker.register('/sw.js');
If the worker sits on a different origin, or in a subfolder with a narrow scope, subscriptions will fail or only work on some pages.
Step 2: ask permission at the right moment
Calling Notification.requestPermission() on page load is the fastest way to get blocked. Chrome now shows quieter prompts for sites with low accept rates. Ask after a user gesture, ideally after a short in-page explanation of what they will receive.
Step 3: subscribe with your VAPID public key
VAPID is a key pair that identifies your application server to the push service. You generate it once and keep the private key secret:
const sub = await reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: VAPID_PUBLIC_KEY
});
The returned subscription contains an endpoint URL (which points at FCM, autopush or Apple) and two keys, p256dh and auth. Send this JSON to your server and store it. Those three values plus your private VAPID key are everything you need to reach that browser later.
Step 4: send from your server
To send, your server:
- encrypts the payload with the subscriber's
p256dhandauthkeys (aes128gcm content encoding), - signs a short JWT with the VAPID private key,
- makes an HTTP POST to the subscription's endpoint with a
TTLheader.
You do not need a Firebase project for standard Web Push with VAPID; Chrome's endpoint on fcm.googleapis.com accepts VAPID-signed requests directly. If you use the Firebase JavaScript SDK instead, the setup differs slightly, and Google's guide to setting up a JavaScript FCM client covers that path, including the move to the HTTP v1 API after the legacy send API was retired.
Step 5: show the notification in the worker
When the push service delivers the message, the browser wakes your service worker:
self.addEventListener('push', event => {
const data = event.data.json();
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: data.icon,
data: { url: data.url }
})
);
});
self.addEventListener('notificationclick', event => {
event.notification.close();
event.waitUntil(clients.openWindow(event.notification.data.url));
});
Always show a notification for every push. With userVisibleOnly: true, browsers may revoke permission from sites that receive pushes silently.
Reading the responses
Your server's response codes tell you a lot:
- 201 Created: accepted for delivery.
- 404 or 410: the subscription is gone. Delete it.
- 413: payload too large; keep it under about 4 KB.
- 429: you are being rate-limited; back off and retry.
Most "push stopped working" tickets come down to never pruning 410s, a moved service worker, or a lost VAPID private key. The last one is unrecoverable, so back it up.
Build it or install it
Writing this flow for one site is a weekend project. Running it reliably for many sites, with segmentation, scheduling, queues and a WordPress integration, is a lot more. If you want the control of owning your keys without writing the plumbing, LaraPush packages this exact flow as a panel you install on your own server, with web push sent via FCM and the keys staying on your VPS. Either way, knowing the moving parts makes you far better at debugging whichever setup you run.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.