How Push API Receives Messages in Service Workers

The Push API enables web applications to receive messages sent from a remote server even when the web application is not actively open in the browser. This article explains the underlying mechanism of this process, covering how subscriptions are established, how messages travel securely from an application server through a browser push service, and how the JavaScript service worker intercepts, decrypts, and processes the incoming push event to display notifications or update local data.


The Push Architecture Components

The Push API relies on four primary components interacting across the network:

  1. User Agent (Browser): The client environment managing user permissions, service workers, and active network connections to the push service.
  2. Service Worker: The background JavaScript worker running independently of web pages, responsible for handling push events.
  3. Push Service: A browser-vendor-hosted infrastructure (e.g., Mozilla Push Service, Google FCM, Apple Push Notification service) that maintains persistent connections with the browser and queues messages.
  4. Application Server: Your backend server that generates notifications and triggers push requests.

Step-by-Step Delivery Flow

1. Creating the Subscription

Before a message can be received, the browser registers a service worker and requests a PushSubscription via pushManager.subscribe(). During this step: - The browser contacts its vendor’s push service. - The push service creates a unique endpoint URL tied to the specific browser instance. - The subscription object generates cryptographic public keys (P-256 curve and an auth secret). - The client sends this subscription data (endpoint and public keys) to your backend application server for storage.

2. Transmitting from Server to Push Service

When your application server needs to send a push message: - It encrypts the payload using the client’s public keys via the RFC 8291 (Message Encryption for Web Push) standard. - It authenticates itself using VAPID (Voluntary Application Server Identification) keys via JSON Web Tokens (JWT). - It sends a standard HTTP POST request containing the encrypted payload directly to the unique endpoint URL provided by the push service.

3. Routing from Push Service to the Browser

The push service receives the HTTP POST request and validates the VAPID headers. It checks if the target device is online: - If the device is offline, the push service queues the message until the device reconnects or the message Time-To-Live (TTL) expires. - If the device is online, the push service routes the encrypted payload through the persistent, low-overhead network connection maintained between the operating system/browser and the push service.

4. Waking the Service Worker

Once the browser receives the message from the push service: 1. The browser decrypts the payload using its stored private key. 2. The browser locates the service worker associated with the subscription. 3. If the service worker is idle or terminated, the browser spins up the service worker environment in the background. 4. The browser dispatches a push event to the service worker’s global execution context.


Handling the Event in the Service Worker

Inside the service worker, a listener handles the incoming push event. The event.data property provides access to the decrypted payload in various formats (text, JSON, blob, or arrayBuffer).

self.addEventListener('push', (event) => {
  let data = {};
  
  if (event.data) {
    data = event.data.json();
  }

  const title = data.title || 'New Notification';
  const options = {
    body: data.body || 'You have received a new update.',
    icon: '/images/icon.png',
    badge: '/images/badge.png',
    data: {
      url: data.url || '/'
    }
  };

  // Keep the service worker alive until the notification is displayed
  event.waitUntil(
    self.registration.showNotification(title, options)
  );
});

Extending Lifetime with event.waitUntil()

Because service workers can be terminated by the browser at any time to conserve memory and battery, the event.waitUntil() method must be used. It takes a JavaScript Promise and signals to the browser that work is ongoing. The browser keeps the service worker running until the Promise resolves—typically when self.registration.showNotification() finishes displaying the system notification.