W3docs

JavaScript Push API and Notifications

The Push API in JavaScript is an essential tool for developers looking to enhance web applications with real-time notifications. This API, combined with the

Introduction to Push API in JavaScript

The Push API in JavaScript lets a web application receive messages pushed from a server, even when the page is closed or the browser is in the background. It is the foundation of web push notifications: news alerts, chat messages, and re-engagement nudges that arrive without the user keeping a tab open.

The Push API never works alone. It builds on two other browser features:

  • A service worker — a background script that stays alive after the page closes and receives the push.
  • The Notifications API — what the service worker uses to actually show the message to the user.

This page covers the full client-side flow: registering a service worker, requesting permission, subscribing with a VAPID key, receiving the push in the service worker, and showing a notification. It also explains where your server fits in.

How the push flow works

The end-to-end path of a single push message looks like this:

  1. Your page registers a service worker and subscribes to push. The browser returns a PushSubscription object containing a unique endpoint URL.
  2. Your page sends that subscription to your server and stores it.
  3. Later, your server signs a message with its VAPID private key and sends it to the subscription endpoint, which belongs to a vendor Push Service (Mozilla, Google, Apple, etc.).
  4. The Push Service wakes the user's browser and fires a push event in your service worker.
  5. The service worker shows a notification in response.

The Push API requires a secure context: the page must be served over HTTPS (localhost is treated as secure for development). Without it, navigator.serviceWorker and PushManager are unavailable.

Implementing Push Notifications

Setting Up Service Workers

First, we need to register a service worker which handles the background tasks of pushing notifications:

// Registering a service worker
if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/service-worker.js')
        .then(function(registration) {
            console.log('Service Worker registered with scope:', registration.scope);
        }).catch(function(error) {
            console.log('Service Worker registration failed:', error);
        });
}

Requesting Permission for Notifications

Before sending notifications, it’s necessary to request permission from the user. The request must be triggered by a user gesture (such as a click) — browsers reject permission prompts that fire automatically on page load:

<button id="enable-notif-btn">Enable Notifications</button>
<script>
    // Asking user permission for notifications
    function requestPermission() {
        Notification.requestPermission().then(function(permission) {
            console.log('Notification permission:', permission);
        });
    }
    document.getElementById('enable-notif-btn').addEventListener('click', requestPermission);
</script>

Subscribing to Push Notifications

After obtaining permission, the application can subscribe to push notifications. The applicationServerKey must be a Uint8Array, not the base64 string you usually store — so convert it first. VAPID (Voluntary Application Server Identification) is the key pair that lets the Push Service verify that pushes really come from your server: the public key goes here, the private key stays on your backend.

<button id="subscribe-btn">Subscribe to Push Notifications</button>
<script>
    // The VAPID public key arrives as a base64url string; the API needs a Uint8Array.
    function urlBase64ToUint8Array(base64String) {
        const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
        const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
        const raw = atob(base64);
        return Uint8Array.from([...raw].map(c => c.charCodeAt(0)));
    }

    const VAPID_PUBLIC_KEY = 'YOUR_VAPID_PUBLIC_KEY'; // base64url string from your server

    function subscribeToPush() {
        navigator.serviceWorker.ready.then(function(registration) {
            // userVisibleOnly: true is required — the browser rejects silent push subscriptions.
            return registration.pushManager.subscribe({
                userVisibleOnly: true,
                applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY)
            });
        })
        .then(function(subscription) {
            console.log('Push subscription:', JSON.stringify(subscription));
            // Send the subscription to your backend so it can push to this user later.
            return fetch('/api/save-subscription', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(subscription)
            });
        })
        .catch(function(error) {
            console.log('Failed to subscribe to push:', error);
        });
    }
    document.getElementById('subscribe-btn').addEventListener('click', subscribeToPush);
</script>

A PushSubscription serializes to JSON containing the endpoint URL and the encryption keys (p256dh and auth). Your server needs all of these to send a message. Setting userVisibleOnly: true is mandatory in current browsers: it promises that every push will result in a user-visible notification, which is why silent background pushes are not allowed on the web.

Handling Incoming Push Messages

To handle incoming messages, the service worker listens for push events. The pushed payload arrives on event.data; use event.data.json() (or .text()) to read it. Wrapping showNotification() in event.waitUntil() keeps the service worker alive until the notification is displayed:

// Inside service-worker.js
self.addEventListener('push', function(event) {
    // Read the payload your server sent (fall back gracefully if there is none).
    var payload = event.data ? event.data.json() : {};

    var options = {
        body: payload.body || 'New notification.',
        icon: 'icon.png',
        vibrate: [100, 50, 100],
        data: { primaryKey: 1 }
    };

    event.waitUntil(
        self.registration.showNotification(payload.title || 'Push Notification', options)
    );
});

self.addEventListener('notificationclick', function(event) {
    event.notification.close();
    event.waitUntil(
        clients.openWindow('https://example.com')
    );
});

self.addEventListener('pushsubscriptionchange', function(event) {
    console.log('Subscription changed, re-subscribing...');
    // Re-subscribe using the same parameters
    event.registration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: 'YOUR_VAPID_PUBLIC_KEY'
    }).then(function(newSubscription) {
        console.log('Re-subscribed:', newSubscription);
    }).catch(function(error) {
        console.error('Re-subscription failed:', error);
    });
});

This snippet shows the three events worth handling: push (display the notification), notificationclick (focus or open a window when the user clicks), and pushsubscriptionchange (re-subscribe when the browser rotates the subscription).

The Server's Role

The browser cannot push to itself — every message originates from your backend. The server keeps the VAPID private key and, for each stored subscription, sends an encrypted, signed HTTP request to the subscription's endpoint. In practice you use a web-push library (for example, web-push on Node.js) rather than building the encryption by hand:

// Server side (Node.js) — conceptual example
const webpush = require('web-push');

webpush.setVapidDetails(
    'mailto:[email protected]',
    process.env.VAPID_PUBLIC_KEY,
    process.env.VAPID_PRIVATE_KEY
);

// `subscription` is the JSON object the browser sent to /api/save-subscription
const payload = JSON.stringify({ title: 'Hello', body: 'You have a new message.' });
webpush.sendNotification(subscription, payload)
    .catch(err => console.error('Push failed:', err.statusCode));

A 410 Gone or 404 response means the subscription has expired — delete it from your database. This is the server-side counterpart to handling pushsubscriptionchange on the client.

Best Practices for Push Notifications

  • User Engagement: Design notifications to be timely, relevant, and precise.
  • Privacy Compliance: Always ensure user consent is obtained before sending notifications.
  • Performance: Manage the frequency and timing of notifications to avoid overwhelming the user.
  • Subscription Renewal: Push subscriptions expire periodically. Implement client-side logic to check subscription status and re-subscribe when necessary, or handle expiration events from the service worker.

Conclusion

The Push API opens a channel for direct interaction with users, providing a powerful tool for engagement. By leveraging this API, developers can deliver a more dynamic and responsive user experience. Proper implementation of push notifications can significantly enhance the functionality and appeal of web applications, keeping users informed and engaged.

Practice

Practice
What are the capabilities and requirements of the JavaScript Push API?
What are the capabilities and requirements of the JavaScript Push API?
Was this page helpful?