Quizzr Logo

Progressive Web Apps (PWA)

Architecting Web Push Notifications with VAPID and Push API

Integrate the Push and Notification APIs to re-engage users with timely, server-triggered updates even when the PWA is not currently open.

Web DevelopmentIntermediate12 min read

The Evolution of Real-Time Web Communication

Traditional web applications operate on a request-response model where the client must initiate every interaction. This architecture creates a significant barrier for applications that require immediate updates, such as instant messaging platforms or financial monitoring tools. Without a persistent connection or a way for the server to reach out, developers often resort to resource-intensive techniques like short polling.

Progressive Web Apps solve this limitation by decoupling the delivery of information from the state of the user interface. By utilizing the Push API, a server can transmit data to a service worker that remains dormant until a message arrives. This mechanism allows the application to respond to external triggers without requiring the browser tab to be active or even open.

The push architecture introduces an intermediary known as the Push Service, which is typically managed by the browser vendor. This service acts as a reliable delivery agent that holds messages for the user until their device is connected to the network. This asynchronous delivery model ensures that critical updates reach the user with minimal impact on battery life and system performance.

The fundamental shift in the PWA model is moving from a client-centric pull architecture to a server-driven push architecture that operates independently of the main execution thread.

The Role of the Service Worker in Messaging

The service worker acts as the backbone of the re-engagement strategy because it provides a persistent execution environment. Unlike standard JavaScript files tied to a window, the service worker lives within a different lifecycle that can be woken up by the browser in response to system events. This independence is what enables the processing of background tasks without user intervention.

When the Push Service receives a message from your application server, it routes that message to the specific browser instance on the user device. The browser then identifies the registered service worker associated with that subscription and fires a push event. This process happens entirely in the background, allowing the app to prepare data before the user even sees a notification.

Establishing Trust with VAPID and Permissions

Security is a primary concern when allowing servers to send unsolicited data to a user device. To prevent malicious actors from spamming users or spoofing application identities, the Web Push protocol requires a voluntary identification system called VAPID. This standard uses a public and private key pair to sign requests, ensuring that only your authorized server can send messages to your users.

Before any push communication can occur, the application must explicitly request permission from the user to show notifications. This permission prompt is a sensitive touchpoint in the user experience and should be triggered in context rather than on the initial page load. Users are far more likely to grant permission if they understand the value of the alerts they will receive.

  • Generate a VAPID key pair consisting of a public key for the client and a private key for the server.
  • Check for existing notification permissions using the Permissions API before showing a custom UI.
  • Provide a clear opt-out mechanism within the application settings to maintain user trust.
  • Ensure the server-side push request includes a TTL value to define how long the message remains valid.

Configuring the VAPID Keys

The public key is shared with the Push Service during the subscription process to associate the client with your specific server. When your server later sends a push message, it signs the payload with the private key to prove its identity. The Push Service then verifies this signature using the public key it stored during the initial handshake.

It is vital to keep the private key secure on your backend environment and never expose it in client-side code. If a private key is compromised, you must rotate the keys and re-subscribe all users, which can lead to a temporary loss of communication. Most modern libraries simplify this process by handling the underlying elliptic curve cryptography automatically.

Managing the Subscription Lifecycle

A subscription is a unique object that contains an endpoint URL and cryptographic keys specific to a single browser on a single device. The process begins by checking if the service worker is active and ready to handle incoming events. Once ready, the application uses the push manager to create a new subscription or retrieve an existing one.

After obtaining the subscription object on the client, it must be transmitted to your backend server for storage. This usually involves a POST request to an API endpoint where you associate the subscription with a specific user account. Since subscriptions can expire or be revoked by the browser, your backend must be capable of updating or removing these records as needed.

javascriptClient-Side Subscription Logic
1async function subscribeUserToPush() {
2  // Wait for the service worker registration to be ready
3  const registration = await navigator.serviceWorker.ready;
4  
5  // Define the subscription options including the VAPID public key
6  const subscribeOptions = {
7    userVisibleOnly: true,
8    applicationServerKey: 'BEl627p_Q9Y1_p_L7F_S-R4-w99X8_L2A8_O7H_A'
9  };
10
11  // Attempt to create the subscription
12  const subscription = await registration.pushManager.subscribe(subscribeOptions);
13  
14  // Send the subscription object to the application server
15  await fetch('/api/save-subscription', {
16    method: 'POST',
17    body: JSON.stringify(subscription),
18    headers: { 'Content-Type': 'application/json' }
19  });
20}

Handling Subscription Expiration

Browsers may occasionally invalidate a push subscription for various reasons, such as long periods of inactivity or changes in the internal push service logic. To ensure reliable delivery, the application should check the validity of the subscription every time the user visits the site. If the subscription has changed or disappeared, the client should automatically re-subscribe and update the server.

You can detect these changes by calling the getSubscription method on the push manager. If the method returns null, it indicates that the subscription is no longer active. Implementing this check during the application bootstrap phase ensures that the bridge between the server and the user remains intact over long periods.

Processing Incoming Data in the Service Worker

When a message arrives from the server, the service worker wakes up and triggers the push event. This event provides a data property that contains the payload sent by your backend, which is typically a JSON object. It is important to remember that the service worker has a limited window of execution time to process this data and display a notification.

The browser requires that a notification be shown to the user if a push message is received while the app is in the background. If you fail to call the showNotification method, the browser may eventually penalize the app by revoking push permissions. This ensures that push messages are always transparent and provide value to the user rather than running hidden tasks.

javascriptService Worker Push Event Handler
1self.addEventListener('push', (event) => {
2  // Parse the incoming JSON payload from the server
3  const data = event.data ? event.data.json() : { title: 'New Update', body: 'Check the app for details.' };
4
5  const options = {
6    body: data.body,
7    icon: '/images/icon-192.png',
8    badge: '/images/badge-72.png',
9    data: {
10      url: data.url // Custom data to use during the click event
11    }
12  };
13
14  // Extend the event lifetime until the notification is shown
15  event.waitUntil(
16    self.registration.showNotification(data.title, options)
17  );
18});

Designing Engaging Notifications

A notification is more than just text; it is an entry point back into your application. You can enhance the visual presentation by adding icons, badges, and high-resolution images. Badges are particularly useful on mobile platforms as they appear in the system status bar, providing a subtle hint that an update is waiting.

Action buttons allow users to interact with the application without fully opening the browser. For example, a task management PWA might include buttons to Mark as Done or Remind Me Later. These actions are handled in the notificationclick event, where you can perform background logic or navigate the user to a specific deep link within the app.

The Importance of event.waitUntil

The service worker environment is ephemeral, meaning the browser can terminate it as soon as it thinks the work is finished. To prevent the worker from being killed before the notification is fully rendered, you must use the event.waitUntil method. This function takes a promise and tells the browser to keep the service worker alive until that promise resolves.

Neglecting to use this method can result in intermittent failures where notifications are never displayed because the service worker was terminated mid-process. This is a common pitfall for developers new to the service worker lifecycle. Always wrap your asynchronous operations, such as fetching additional data or displaying the UI, within this protective wrapper.

Actionable Engagement and Operational Resilience

The ultimate goal of a notification is to bring the user back to a relevant part of the application. Handling the notificationclick event is the final step in the re-engagement loop. This event allows you to identify which notification was clicked, which action was taken, and how the application should respond visually.

In many cases, the most appropriate response is to open a specific URL or focus an existing window that is already open to that page. The clients API within the service worker provides the necessary tools to query open windows and navigate them to the target destination. This creates a fluid experience that feels native to the operating system.

javascriptHandling Notification Interaction
1self.addEventListener('notificationclick', (event) => {
2  const notification = event.notification;
3  const action = event.action;
4  
5  notification.close(); // Always close the notification after a click
6
7  if (action === 'dismiss') {
8    return; // User clicked the dismiss action
9  }
10
11  // Open the target URL defined in the push payload
12  event.waitUntil(
13    clients.matchAll({ type: 'window', includeUncontrolled: true }).then((windowClients) => {
14      // Check if a tab is already open and focus it
15      for (const client of windowClients) {
16        if (client.url === notification.data.url && 'focus' in client) {
17          return client.focus();
18        }
19      }
20      // If no matching tab, open a new one
21      if (clients.openWindow) {
22        return clients.openWindow(notification.data.url);
23      }
24    })
25  );
26});

Managing Payload Constraints

Push messages have a maximum payload size of approximately 4 kilobytes. This limitation means you cannot send large amounts of data or images directly through the push channel. Instead, the payload should contain a small JSON object with just enough information to identify the event and perhaps a URL to fetch more data.

If your notification requires complex data that isn't in the payload, you can perform a fetch request within the service worker push event. However, keep in mind that network conditions may be poor. Designing your system to work with the minimal data provided in the initial push payload is the most robust strategy for high-performance PWAs.

Avoiding Notification Fatigue

While push notifications are powerful, they are easily abused. Sending too many low-value alerts will lead users to disable notifications entirely or uninstall the application. Implement a frequency capping strategy on your server to ensure that users are only notified about truly significant events.

Consider grouping multiple related updates into a single summary notification if they occur within a short timeframe. Most operating systems support notification tagging, which allows you to replace an old notification with a new one rather than stacking them. This keeps the user notification center clean and organized, respecting their digital space.

We use cookies

Necessary cookies keep the site working. Analytics and ads help us improve and fund Quizzr. You can manage your preferences.