Go back Web Push and VAPID: Breaking Down the End-to-End Flow /* by Ayush Makwana - February 6, 2026 */ Tech Update Web Push allows a server to deliver notifications to the browser, even when the application is not open. Once permission is granted, messages can be delivered without an active page or a live connection. What enables this is not a single API call, but coordination between the client, the backend, and a push service in the middle. Two primitives sit at the center of this coordination: subscription-based encryption keys that ensure message privacy, and a VAPID key pair that establishes the identity and authorization of the sending server. The Actors (Who Does What) Before code, we need to know who is responsible for what. ActorResponsibilityFrontend (FE)Ask permission, initiate subscriptionBrowser RuntimeTalks to push service, manages keysPush Service (PS)Verifies sender, delivers messagesBackend (BE)Encrypts payload, signs requestService Worker (SW)Receives push, shows notification Web Push lifecycle showing frontend, backend, push service, and service worker interactions. Step 1: VAPID Keys (Server Identity) VAPID (Voluntary Application Server Identification) provides a way for your backend to prove its identity to the push service. It establishes that your server is authorized to send push messages for this application. We generate one VAPID key pair: Public key that is shared with frontend Private key that stays on backend NOTE: VAPID does not encrypt messages. Step 2: Frontend Subscribes the Browser The frontend fetches the public VAPID key from your backend and provides it to the browser during subscription: const permission =awaitNotification.requestPermission()if (permission !=='granted')returnconst reg =await navigator.serviceWorker.readyconst subscription =await reg.pushManager.subscribe({userVisibleOnly:true,applicationServerKey: vapidPublicKeyUint8Array,}) What happens internally: The browser talks to the push service The push service: creates an endpoint generates encryption keys The browser returns a PushSubscription object { "endpoint": "<https://fcm.googleapis.com/>...", "keys": { "p256dh": "...", "auth": "..." }} Step 3: Frontend Sends Subscription to Backend The subscription object is not a token. It contains: the endpoint (address), the encryption keys needed for payload privacy. Send it to your backend: await fetch('/api/subscribe', { method: 'POST', body: JSON.stringify(subscription),}) The backend stores this subscription as-is for future use. Why There Are Two Cryptographic Steps Web Push uses two distinct forms of cryptography for different purposes. Understanding this distinction is central to a correct implementation. 1. Payload Encryption (Privacy) Ensure the push service cannot read your message. Uses subscription keys (p256dh, auth) Generated by the push service during subscription Only the browser can decrypt This protects message content. 2. VAPID Signing (Authentication) Prove to the push service that you are allowed to send. Backend signs a JWT with private VAPID key Push service verifies with public VAPID key This protects sender identity. Step 4: Backend Sends a Push Notification When you want to deliver a notification, the backend performs two key tasks: Encrypt the payload using the subscription’s encryption keys Sign the request with your private VAPID key Conceptually: // pseudo-codeencryptPayload(subscription.keys, payload)signRequestWithVapid(privateKey)POST encryptedPayload to subscription.endpoint The backend never communicates directly with the browser; it always sends through the push service. Step 5: Push Service Verifies and Delivers Upon receiving the request from your backend, the push service: Verifies the VAPID JWT Confirms the endpoint is valid Queues the encrypted payload Delivers it to the browser If any step fails, you get: 403 means invalid VAPID 410 means subscription expired Silent drops (yes, really) Step 6: Browser Receives the Push When the push arrives at the browser, even if the site is closed. the service worker handles it: self.addEventListener('push',event => { const data = event.data.json() self.registration.showNotification(data.title, { body: data.body, })}) The browser decrypts the payload using the subscription keys, wakes the service worker, and displays the notification. Conclusion Web Push works because each part of the system has a clear responsibility. VAPID establishes the identity of the sender, subscription keys protect the message content, and the push service takes care of delivery. Viewed this way, the system becomes easier to reason about.