How JavaScript Push API Manages Push Notifications

JavaScript manages push notifications through a coordinated workflow between the Push API, the Notifications API, and Service Workers. This architecture enables web applications to receive messages from a backend server and display interactive notifications on a user’s device, even when the web page is not actively open in the browser. The following breakdown explains the core components, registration process, and event handling that make JavaScript push notifications functional.

1. The Three Core Components

Push messaging relies on three distinct layers: * The Client Application and Service Worker: JavaScript running in the browser and in a background thread (Service Worker) that listens for network events. * The Application Server: Your backend server, which creates notifications and sends them to the push service. * The Push Service: A browser-vendor-operated server (such as Google FCM or Mozilla Autopush) that routes messages to the specific browser instance.

2. Requesting User Permission

Before subscribing to push messages, the application must obtain explicit user consent using the Notifications API.

async function requestNotificationPermission() {
  const permission = await Notification.requestPermission();
  if (permission !== 'granted') {
    throw new Error('Notification permission denied');
  }
  return permission;
}

3. Registering the Service Worker and Subscribing

Once permission is granted, JavaScript registers a Service Worker. The Service Worker exposes the PushManager interface, which allows the client to subscribe to the vendor’s Push Service using a public VAPID (Voluntary Application Server Identification) key.

async function subscribeUserToPush() {
  const registration = await navigator.serviceWorker.register('/sw.js');
  const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: urlBase64ToUint8Array('YOUR_PUBLIC_VAPID_KEY')
  });

  // Send the subscription object to your backend database
  await fetch('/api/save-subscription', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(subscription)
  });
}

The resulting PushSubscription object contains an endpoint URL and cryptographic encryption keys (p256dh and auth), which your backend server stores to address messages to this specific client.

4. Sending a Push Message from the Server

When your backend needs to send a notification, it encrypts the payload and sends an HTTP POST request to the subscription endpoint using standard Web Push protocols. The browser vendor’s Push Service receives this request, identifies the target device, and queues or delivers the message.

5. Handling the push Event in the Service Worker

When the message reaches the device, the browser wakes up the registered Service Worker (if it is not already running) and fires a push event. The Service Worker extracts the payload and displays the notification using showNotification().

// sw.js
self.addEventListener('push', (event) => {
  let data = { title: 'New Notification', body: 'You have an update.' };

  if (event.data) {
    data = event.data.json();
  }

  const options = {
    body: data.body,
    icon: '/images/icon.png',
    badge: '/images/badge.png',
    data: { url: data.url || '/' }
  };

  event.waitUntil(
    self.registration.showNotification(data.title, options)
  );
});

The event.waitUntil() method ensures the browser does not terminate the Service Worker before the notification has been successfully rendered.

6. Handling User Interaction

JavaScript handles clicks on the generated notification through the notificationclick event inside the Service Worker, allowing developers to focus an existing browser tab or open a new URL.

// sw.js
self.addEventListener('notificationclick', (event) => {
  event.notification.close();

  const targetUrl = event.notification.data.url;

  event.waitUntil(
    clients.matchAll({ type: 'window', includeUncontrolled: true }).then((windowClients) => {
      for (const client of windowClients) {
        if (client.url === targetUrl && 'focus' in client) {
          return client.focus();
        }
      }
      if (clients.openWindow) {
        return clients.openWindow(targetUrl);
      }
    })
  );
});