Subscribe a user to push notifications

Matt Gaunt

To send push messages, you must first get permission from the user and then subscribe their device to a push service. This involves using the JavaScript API to obtain a PushSubscription object, which you then send to your server.

The JavaScript API manages this process straightforwardly. This guide explains the entire flow, including feature detection, requesting permission, and managing the subscription process.

Feature detection

First, check if the browser supports push messaging. You can check for push support with two checks:

  • Check for serviceWorker on the navigator object.
  • Check for PushManager on the window object.
if (!('serviceWorker' in navigator)) {
  // Service Worker isn't supported on this browser, disable or hide UI.
  return;
}

if (!('PushManager' in window)) {
  // Push isn't supported on this browser, disable or hide UI.
  return;
}

While browser support for both service worker and push messaging is increasing, always detect both features and progressively enhance your application.

Register a service worker

After feature detection, you know that service workers and push messaging are supported. Next, register your service worker.

When you register a service worker, you tell the browser the location of your service worker file. The file is a JavaScript file, but the browser grants it access to service worker APIs, including push messaging. Specifically, the browser runs the file in a service worker environment.

To register a service worker, call navigator.serviceWorker.register() and pass the path to your file. For example:

function registerServiceWorker() {
  return navigator.serviceWorker
    .register('/service-worker.js')
    .then(function (registration) {
      console.log('Service worker successfully registered.');
      return registration;
    })
    .catch(function (err) {
      console.error('Unable to register service worker.', err);
    });
}

This function tells the browser the location of your service worker file. Here, the service worker file is at /service-worker.js. After you call register(), the browser performs these steps:

  1. Download the service worker file.

  2. Run the JavaScript.

  3. If the file runs correctly without errors, the promise returned by register() resolves. If errors occur, the promise rejects.

Note: If register() rejects, check your JavaScript for typos or errors in Chrome DevTools.

When register() resolves, it returns a ServiceWorkerRegistration. You use this registration to access the PushManager API.

PushManager API browser compatibility

Browser Support

  • Chrome: 42.
  • Edge: 17.
  • Firefox: 44.
  • Safari: 16.

Source

Requesting permission

After registering your service worker and obtaining permission, get permission from the user to send push messages.

The API for getting permission is simple. However, the API recently changed from taking a callback to returning a Promise. Because you cannot determine which API version the browser implements, you must implement and handle both versions.

function askPermission() {
  return new Promise(function (resolve, reject) {
    const permissionResult = Notification.requestPermission(function (result) {
      resolve(result);
    });

    if (permissionResult) {
      permissionResult.then(resolve, reject);
    }
  }).then(function (permissionResult) {
    if (permissionResult !== 'granted') {
      throw new Error("We weren't granted permission.");
    }
  });
}

In the preceding code, the call to Notification.requestPermission() displays a prompt to the user:

Permission prompt displayed on desktop and mobile Chrome.

After the user interacts with the permission prompt by selecting Allow, Block, or closing it, you receive the result as a string: 'granted', 'default', or 'denied'.

In the sample code, the promise returned by askPermission() resolves if permission is granted; otherwise, it throws an error and the promise rejects.

Handle the edge case where the user clicks the Block button. If this occurs, your web app cannot ask the user for permission again. The user must manually unblock your app by changing its permission state in a settings panel. Carefully consider when and how to ask for permission, because if a user clicks Block, reversing that decision is not easy.

Most users grant permission if they understand why the app requests it.

This document discusses how some popular sites ask for permission later in this document.

Subscribe a user with PushManager

After registering your service worker and obtaining permission, you can subscribe a user by calling registration.pushManager.subscribe().

function subscribeUserToPush() {
  return navigator.serviceWorker
    .register('/service-worker.js')
    .then(function (registration) {
      const subscribeOptions = {
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array(
          'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U',
        ),
      };

      return registration.pushManager.subscribe(subscribeOptions);
    })
    .then(function (pushSubscription) {
      console.log(
        'Received PushSubscription: ',
        JSON.stringify(pushSubscription),
      );
      return pushSubscription;
    });
}

When you call the subscribe() method, you pass an options object that consists of required and optional parameters.

This section describes the options you can pass.

userVisibleOnly options

When push messaging was first added to browsers, developers were uncertain about sending push messages without displaying a notification. This is commonly referred to as silent push because the user does not know that an event occurred in the background.

The concern was that developers could track a user's location continuously without the user's knowledge.

To avoid this scenario and allow spec authors to consider how best to support this feature, the userVisibleOnly option was added. Passing a value of true is a symbolic agreement with the browser that the web app displays a notification every time it receives a push message (that is, no silent push).

You must pass a value of true. If you do not include the userVisibleOnly key or pass false, you receive the following error:

Chrome currently only supports the Push API for subscriptions that will result
in user-visible messages. You can indicate this by calling
`pushManager.subscribe({userVisibleOnly: true})` instead. See
[https://goo.gl/yqv4Q4](https://goo.gl/yqv4Q4) for more details.

Chrome supports the Push API only for subscriptions that result in user-visible messages. Indicate this by calling pushManager.subscribe({userVisibleOnly: true}). For more information, see https://goo.gl/yqv4Q4.

It appears that blanket silent push will not be implemented in Chrome. Instead, spec authors are exploring a budget API that lets web apps send a certain number of silent push messages based on web app usage.

applicationServerKey option

This document previously mentioned application server keys. A push service uses application server keys to identify the application subscribing a user and to ensure that the same application messages that user.

Application server keys are a public and private key pair that are unique to your application. Keep the private key secret to your application, and share the public key freely.

The applicationServerKey option passed into the subscribe() call is your application's public key. The browser passes this key to a push service when subscribing the user, which enables the push service to tie your application's public key to the user's PushSubscription.

The following diagram illustrates these steps.

  1. Load your web app in a browser and call subscribe(), passing your public application server key.
  2. The browser then makes a network request to a push service, which generates an endpoint, associates this endpoint with your application's public key, and returns the endpoint to the browser.
  3. The browser adds this endpoint to the PushSubscription, which the subscribe() promise returns.

Diagram illustrating how the public application server key is used in the `subscribe()` method.

When you send a push message, create an Authorization header that contains information signed with your application server's private key. When the push service receives a request to send a push message, it validates this signed Authorization header by looking up the public key linked to the endpoint receiving the request. If the signature is valid, the push service knows that the request came from the application server with the matching private key. This is a security measure that prevents others from sending messages to your application's users.

Diagram illustrating how the private application server key is used when sending a message.

Technically, the applicationServerKey is optional. However, the simplest implementation on Chrome requires it, and other browsers might require it in the future. It's optional on Firefox.

The VAPID spec defines the application server key. When you see references to application server keys or VAPID keys, remember that they are the same.

Create application server keys

You can create a public and private set of application server keys by visiting web-push-codelab.glitch.me or by using the web-push command line to generate keys as follows:

    $ npm install -g web-push
    $ web-push generate-vapid-keys

Create these keys only once for your application, and ensure you keep the private key private.

Permissions and subscribe()

Calling subscribe() has one side effect. If your web app does not have permission to show notifications when you call subscribe(), the browser requests the permissions for you. This is useful if your UI works with this flow, but if you want more control (which most developers do), use the Notification.requestPermission() API that this document discussed earlier.

PushSubscription overview

You call subscribe(), pass options, and receive a promise that resolves to a PushSubscription. For example:

function subscribeUserToPush() {
  return navigator.serviceWorker
    .register('/service-worker.js')
    .then(function (registration) {
      const subscribeOptions = {
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array(
          'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U',
        ),
      };

      return registration.pushManager.subscribe(subscribeOptions);
    })
    .then(function (pushSubscription) {
      console.log(
        'Received PushSubscription: ',
        JSON.stringify(pushSubscription),
      );
      return pushSubscription;
    });
}

The PushSubscription object contains all the information required to send push messages to that user. If you print the contents using JSON.stringify(), you see the following:

    {
      "endpoint": "https://some.pushservice.com/something-unique",
      "keys": {
        "p256dh":
    "BIPUL12DLfytvTajnryr2PRdAgXS3HGKiLqndGcJGabyhHheJYlNGCeXl1dn18gSJ1WAkAPIxr4gK0_dQds4yiI=",
        "auth":"FPssNDTKnInHVndSTdbKFw=="
      }
    }

The endpoint is the push service's URL. To trigger a push message, make a POST request to this URL.

The keys object contains the values used to encrypt message data sent with a push message. (This document discusses message encryption later.)

Send a subscription to your server

After you have a push subscription, send it to your server. You determine how to send it, but a tip is to use JSON.stringify() to extract all necessary data from the subscription object. Alternatively, you can manually assemble the same result, for example:

const subscriptionObject = {
  endpoint: pushSubscription.endpoint,
  keys: {
    p256dh: pushSubscription.getKeys('p256dh'),
    auth: pushSubscription.getKeys('auth'),
  },
};

// The above is the same output as:

const subscriptionObjectToo = JSON.stringify(pushSubscription);

To send the subscription from the web page, use the following:

function sendSubscriptionToBackEnd(subscription) {
  return fetch('/api/save-subscription/', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(subscription),
  })
    .then(function (response) {
      if (!response.ok) {
        throw new Error('Bad status code from server.');
      }

      return response.json();
    })
    .then(function (responseData) {
      if (!(responseData.data && responseData.data.success)) {
        throw new Error('Bad response from server.');
      }
    });
}

The Node.js server receives this request and saves the data to a database for later use.

app.post('/api/save-subscription/', function (req, res) {
  if (!isValidSaveRequest(req, res)) {
    return;
  }

  return saveSubscriptionToDatabase(req.body)
    .then(function (subscriptionId) {
      res.setHeader('Content-Type', 'application/json');
      res.send(JSON.stringify({data: {success: true}}));
    })
    .catch(function (err) {
      res.status(500);
      res.setHeader('Content-Type', 'application/json');
      res.send(
        JSON.stringify({
          error: {
            id: 'unable-to-save-subscription',
            message:
              'The subscription was received but we were unable to save it to our database.',
          },
        }),
      );
    });
});

With the PushSubscription details on your server, you can send your user a message at any time.

Resubscribe regularly to prevent expiration

When subscribing to push notifications, you often receive a PushSubscription.expirationTime of null. In theory, this means the subscription never expires. (In contrast, a DOMHighResTimeStamp indicates the exact expiration time.) In practice, however, browsers commonly let subscriptions expire. For example, this can occur if no push notifications are received for a long time, or if the browser detects the user is not using an app that has push notification permission. One pattern to prevent this is to resubscribe the user upon each received notification, as the following snippet shows. This requires you to send notifications frequently enough to prevent the browser from auto-expiring the subscription. You should carefully weigh the advantages and disadvantages of legitimate notification needs against involuntarily spamming the user solely to prevent subscription expiration. Ultimately, you should not try to circumvent the browser's efforts to protect the user from long-forgotten notification subscriptions.

/* In the Service Worker. */

self.addEventListener('push', function(event) {
  console.log('Received a push message', event);

  // Display notification or handle data
  // Example: show a notification
  const title = 'New Notification';
  const body = 'You have new updates!';
  const icon = '/images/icon.png';
  const tag = 'simple-push-demo-notification-tag';

  event.waitUntil(
    self.registration.showNotification(title, {
      body: body,
      icon: icon,
      tag: tag
    })
  );

  // Attempt to resubscribe after receiving a notification
  event.waitUntil(resubscribeToPush());
});

function resubscribeToPush() {
  return self.registration.pushManager.getSubscription()
    .then(function(subscription) {
      if (subscription) {
        return subscription.unsubscribe();
      }
    })
    .then(function() {
      return self.registration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array('YOUR_PUBLIC_VAPID_KEY_HERE')
      });
    })
    .then(function(subscription) {
      console.log('Resubscribed to push notifications:', subscription);
      // Optionally, send new subscription details to your server
    })
    .catch(function(error) {
      console.error('Failed to resubscribe:', error);
    });
}

Frequently asked questions

Here are some common questions:

Can you change the push service a browser uses?

No, the browser selects the push service. As this document discussed with the subscribe() call, the browser makes network requests to the push service to retrieve the details that make up the PushSubscription.

Do different push services use different APIs?

All push services expect the same API.

This common API, called the Web Push Protocol, describes the network request your application makes to trigger a push message.

If you subscribe a user on their desktop, are they subscribed on their phone as well?

No. A user must register for push messaging on each browser where they want to receive messages. This also requires the user to grant permission on each device.

Next steps

Codelabs