Back

Implementing Push Notifications with the Web Push API

Implementing Push Notifications with the Web Push API

Push notifications keep users engaged even when they’re not actively using your web application. The Web Push API enables you to send timely updates directly to users’ devices without relying on third-party services. This guide shows how to implement native-like notifications using Service Workers, VAPID authentication, and proper encryption.

Key Takeaways

  • Web Push API enables native push notifications without third-party services
  • Service Workers and VAPID authentication are essential components
  • HTTPS connection is mandatory for implementation
  • Different browsers have varying requirements for push notifications

Prerequisites and Browser Support

Before implementing push notifications, ensure your application meets these requirements:

  • HTTPS connection (required for Service Workers)
  • Modern browser support (Chrome, Firefox, Edge, Safari 16.4+)
  • Service Worker compatibility
// Feature detection
if ('serviceWorker' in navigator && 'PushManager' in window) {
  // Push notifications supported
}

Registering a Service Worker

The Service Worker acts as a proxy between your web app and push services. Register it during page load:

navigator.serviceWorker.register('/sw.js')
  .then(registration => {
    console.log('Service Worker registered:', registration);
  })
  .catch(error => {
    console.error('Registration failed:', error);
  });

Setting Up VAPID Keys

VAPID (Voluntary Application Server Identification) authenticates your server with push services. Generate a key pair once for your application:

# Using web-push library (Node.js)
npm install web-push
npx web-push generate-vapid-keys

Store the private key securely on your server and use the public key in your client-side subscription.

Creating Push Subscriptions

Request permission and subscribe users to push notifications:

async function subscribeToPush() {
  const registration = await navigator.serviceWorker.ready;
  
  // Request permission (only on user gesture)
  const permission = await Notification.requestPermission();
  if (permission !== 'granted') return;
  
  // Subscribe with VAPID public key
  const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY)
  });
  
  // Send subscription to your server
  await fetch('/api/subscribe', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(subscription)
  });
}

// Helper function to convert base64 to Uint8Array
function urlBase64ToUint8Array(base64String) {
  const padding = '='.repeat((4 - base64String.length % 4) % 4);
  const base64 = (base64String + padding)
    .replace(/\-/g, '+')
    .replace(/_/g, '/');
  
  const rawData = window.atob(base64);
  const outputArray = new Uint8Array(rawData.length);
  
  for (let i = 0; i < rawData.length; ++i) {
    outputArray[i] = rawData.charCodeAt(i);
  }
  return outputArray;
}

The subscription object contains:

  • Endpoint: Unique URL for sending messages
  • Keys: Encryption keys (p256dh and auth) for payload security

Server-Side Implementation

Your server must encrypt messages and send them to the push service endpoint. Using the web-push library simplifies this:

const webpush = require('web-push');

webpush.setVapidDetails(
  'mailto:your-email@example.com',
  VAPID_PUBLIC_KEY,
  VAPID_PRIVATE_KEY
);

// Send notification
const payload = JSON.stringify({
  title: 'New Message',
  body: 'You have a new update',
  icon: '/icon-192.png',
  url: 'https://example.com/updates'
});

webpush.sendNotification(subscription, payload, {
  TTL: 86400, // 24 hours
  urgency: 'high'
})
  .catch(error => {
    console.error('Error sending notification:', error);
  });

Handling Push Events in Service Worker

The Service Worker receives and displays push notifications:

// sw.js
self.addEventListener('push', event => {
  const data = event.data?.json() || {};
  
  const options = {
    body: data.body || 'Default message',
    icon: data.icon || '/icon.png',
    badge: '/badge.png',
    vibrate: [200, 100, 200],
    data: { url: data.url }
  };
  
  event.waitUntil(
    self.registration.showNotification(data.title || 'Notification', options)
  );
});

// Handle notification clicks
self.addEventListener('notificationclick', event => {
  event.notification.close();
  
  event.waitUntil(
    clients.openWindow(event.notification.data?.url || '/')
  );
});

Browser-Specific Requirements

Different browsers enforce varying requirements for Web Push API implementation:

  • Chrome/Edge: Require visible notifications for every push message
  • Firefox: Allows limited silent push with quota system
  • Safari: Requires visible notifications and no silent push support

Note: Payload sizes are limited: Chrome/Edge/Firefox support up to 4KB, while Safari supports 2KB. Keep messages lightweight and fetch additional data in-app if needed.

Always display notifications immediately upon receiving push events to maintain permissions across all browsers.

Security Best Practices

Protect your push notification implementation:

  1. Secure endpoints: Never expose subscription endpoints publicly
  2. Encrypt payloads: All message data must use ECDH encryption
  3. Protect VAPID keys: Store them securely, and regenerate only if compromised
  4. Implement CSRF protection: Validate subscription requests
  5. Rate limiting: Prevent abuse with server-side throttling

Managing Subscription Lifecycle

Handle subscription changes and expiration:

// In Service Worker
self.addEventListener('pushsubscriptionchange', event => {
  event.waitUntil(
    self.registration.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY)
    })
      .then(subscription => {
        // Update server with new subscription
        return fetch('/api/update-subscription', {
          method: 'PUT',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(subscription)
        });
      })
  );
});

Conclusion

The Web Push API provides a standards-based approach to implementing push notifications without vendor lock-in. By combining Service Workers, VAPID authentication, and proper encryption, you can deliver timely notifications that work across modern browsers. Remember to respect user preferences, handle subscription lifecycle events, and follow platform-specific requirements for a robust implementation.

Start with basic notification delivery, then add features like action buttons, image attachments, and notification grouping as your implementation matures.

FAQs

No, push notifications require an active internet connection to receive messages from the push service. However, Service Workers can cache notifications and display them when connectivity returns.

When permission is denied, you cannot send push notifications to that user. You must wait for them to manually change permissions in browser settings. Consider implementing alternative engagement methods like in-app notifications.

Most push services limit payload size to 4KB. Chrome and Firefox support up to 4KB, while Safari supports up to 2KB. Keep payloads minimal and fetch additional data when the notification is received if needed.

Yes, push notifications can be received when the browser is closed on most platforms. However, this depends on the operating system and browser. Mobile browsers may have restrictions based on battery optimization settings.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay