Keep pushing it, with the W3C Push API

You are all familiar with this experience — a little bubble pops up on your phone without warning, containing a nagging message along the lines of “your insipidly cute little monsters are rested, and want to go and fight more battles!”, or “You’ve got unanswered friend requests from people you don’t know. Hurry up and reply!”

Push messages are definitely not a new concept, having been a popular feature of mobile platforms for years. It is only recently however that Push has come to the web platform. This article takes you through the basics, and outlines the current status of Push.

The advantage of Push over other technology choices for sending messages to users is that push messages can allow users to opt in to updates without any intervention from the client whatsoever — as long as the server has the endpoint (see below), it can send updates to a subscribed client only when required (i.e. it doesn’t require a constant connection like sockets, which is good for battery life, etc.)

Note: MDN includes full reference docs for the Push API as well as a detailed tutorial, Using the Push API.

Browser support

The Push API is currently at Working Draft stage. (The latest editor’s draft is here.) Most of the API is supported in recent release versions of Chrome (42+), and Firefox (42+) in pre-release channels only for now (e.g. Nightly, Developer Edition, and Beta). The exception is PushMessageData, which is currently supported only in Firefox 44+ (again not in release channels.)

Push requires service workers to run, which are also mostly supported in the latest versions of Chrome and Firefox (although disabled by default in the latest Firefox release version.) Since service workers require https to work for security purposes, Push also requires https.

It is worth noting that Push is often used along with communication APIs like Web Notifications and Channel Messaging for communicating the results of Push messages being sent. These both have fairly widespread support across modern browsers.

The process of push

In this section we will go through a typical process by which an app utilizing push messages could be set up.

Note: You can find a demo showing the Push API in use on Github. It will be useful for you to get this running so you can follow along as you read the sections below.

Requesting permission

The first thing you should do is request permission for web notifications, or anything else you are using that requires permissions. For example:

Notification.requestPermission();

Registering a service worker and subscribing to push

The next thing is to register a service worker to control the page, using ServiceWorkerContainer.register(). This returns a promise that resolves with a ServiceWorkerRegistration object:

navigator.serviceWorker.register('sw.js').then(function(reg) {
  // do something with reg object
});

Once we have this registration object, we can start registering for Push. Often you will send the ServiceWorkerRegistration object to some kind of subscription function.

At any point we can check that the service worker is active and ready to start doing things using the ServiceWorkerContainer.ready property, which returns a promise that resolves with the aforementioned ServiceWorkerRegistration object. Once we have this, we can access the PushManager using the ServiceWorkerRegistration.pushManager property. Then we can subscribe to the push service using PushManager.subscribe(), which returns a promise that resolves with a PushSubscription object.

navigator.serviceWorker.ready.then(function(reg) {
  reg.pushManager.getSubscription()
  .then(function(subscription) {
    // do something with your PushSubscription object
  });
}

To unsubscribe, the code is similar, but you must use the PushSubscription.unsubscribe() method to unsubscribe from push:

navigator.serviceWorker.ready.then(function(reg) {
  reg.pushManager.getSubscription()
  .then(function(subscription) {
    subscription.unsubscribe().then(function(successful) {
      // unsubscribe was successful
    })
  });
}

Push servers, and sending push messages

To send push messages, you need a server component. This can be written in any server-side language you like, as long as it can handle secure requests/responses and data encryption. (Push messages require https, and data sent by push needs to be encrypted.)

Note: Support for PushMessageData and encryption is currently Firefox-only, with the encryption process still being worked out. (Encryption and getKey() don’t yet appear in the spec.)

Once we have our PushSubscription object, we need to grab two pieces of information that are used for sending push messages:

  • PushSubscription.endpoint: This is a unique URL pointing at the push server that handles sending the push messages (each browser will have its own push server). For example: https://updates.push.services.mozilla.com/push/gAAAAABWJ-VZaQ9DhwvjZJHEHlZCzNJBPTPAcucU9mprtyzisSow75qHbY5lrjglEXE7G6SIfWvz-QSwhBcjpRjx2PAnKCAHd-5XHh1RFXa1ngqq_2-I0-PZoEqigI7E3ISO5zE1tNy29_Iyiu06m0tc_2nfKyuEcjwDPLyOC8c3IvawhBUUzMM=. This is used by your server to send the push message — the request hits the push server, and the random string on the end makes sure the push message is sent to the service worker associated with that particular push subscription.
  • PushSubscription.getKey('p256dh'): This method generates a public client key, one of the components used to encrypt the data. These details should then be sent to the server so it can send push messages when required. (You could use Fetch or XMLHttpRequest to do this.)

On the server-side, you should store the endpoint and any other required details so they are available when a push message needs to be sent to a push subscriber (use a database or whatever you like). In a production app, make sure these details are hidden, so malicious parties can’t steal endpoints and spam subscribers with push messages. Anyone with the endpoint can send a push message, as long as the subscription stays alive.

To send a push message without data, you need to send it to the endpoint URL with a method of POST. To send it with data in Firefox, you need to encrypt it, which involves the client public key. This is a pretty complex procedure (read Message Encryption for Web Push for more details). As time goes on, libraries will be written to do this kind of thing for you; Marco Castelluccio’s NodeJS web-push library is a good current option for NodeJS.

The service worker, and responding to push messages

Over in your service worker, you need to set up an onpush handler to respond to push messages being received.

self.addEventListener('push', function(event) {
  var obj = event.data.json();
  // do something with JSON
});

Note that the event object is of type PushEvent; its data property contains a PushMessageData object, which contains the data sent over the push message. This object has methods available for returning the message payload as a blob, array buffer, JSON object, or plain text string (we are converting it to JSON above). Once you have the payload, you can do what you want with it.

Sending a channel message

If you want respond to the push event by sending a channel message back to the main context, you first need to open a message channel between the main context and the service worker. In the main context, you can do something like this:

navigator.serviceWorker.ready.then(function(reg) {
  var channel = new MessageChannel();
  channel.port1.onmessage = function(e) {
    handleChannelMessage(e.data);
  }

  mySW = reg.active;
  mySW.postMessage('hello', [channel.port2]);
});

First we create a new MessageChannel object using a constructor, then set up an onmessage handler to handle messages coming across the channel to the main context.

Then, as before, we get a reference to our ServiceWorkerRegistration object. We then use its active property to return a ServiceWorker object. We can use the ServiceWorker object’s postMessage() method to post a message to the service worker context, along with port2 of the message channel.

Over in the service worker, we grab a reference to port2 using the following:

var port;

self.onmessage = function(e) {
  port = e.ports[0];
}

Once this link is established, data can be sent back to the main context using:

port.postMessage('my message');

Firing a notification

If you want to respond by firing a system notification, you can do this using ServiceWorkerRegistration.showNotification:

function fireNotification(obj, event) {
  var title = 'Subscription change';
  var body = obj.name + ' has ' + obj.action + 'd.';
  var icon = 'push-icon.png';
  var tag = 'push';

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

Note that here we have run this inside an ExtendableEvent.waitUntil method — this extends the lifetime of the event until after the notification has been fired, so we can make sure everything has happened as we intended.

Handling premature subscription expiration

Sometimes push subscriptions expire prematurely, without unsubscribe() being called. This can happen when the server gets overloaded, or if you are offline for a long time.  This is highly server-dependent, so the exact behavior is difficult to predict. In any case, you can handle this problem using the onsubscriptionchange handler, which will be invoked only in this specific case.

self.addEventListener('subscriptionchange', function() {
  // do something, usually resubscribe to push and
  // send the new subscription details back to the
  // server via XHR or Fetch
});

Chrome support for Push

Chrome has good support for Push as well, but with a few differences from Firefox. For a start, it doesn’t yet support sending PushMessageData in push messages; it also relies on the Google Cloud Messaging service. Read Extra steps for Chrome support for full details.

About Chris Mills

Chris Mills is a senior tech writer at Mozilla, where he writes docs and demos about open web apps, HTML/CSS/JavaScript, A11y, WebAssembly, and more. He loves tinkering around with web technologies, and gives occasional tech talks at conferences and universities. He used to work for Opera and W3C, and enjoys playing heavy metal drums and drinking good beer. He lives near Manchester, UK, with his good lady and three beautiful children.

More articles by Chris Mills…


11 comments

  1. PhistucK

    Just a quick note and sort of corrections –
    Chrome (42) supports the Push API by default (your phrasing suggests that it supports it only in early release channels).
    Service Workers are indeed supported by default in Chrome (40), but they are disabled by default in Firefox (even in the latest stable version).

    October 26th, 2015 at 14:09

    1. Chris Mills

      Thanks PhistucK! You are right that my wording was a bit misleading. I’ve updated it so it is hopefully now better.

      October 27th, 2015 at 01:38

  2. Chao

    Can someone explain to me why we needed a separate API for this, instead of using something like WebSockets within a Service Worker?

    October 26th, 2015 at 15:36

    1. Chris Mills

      This is a useful concern to bring up, Chao. I think PhistucK just beat me to it with the below comment, but I’ve also added a clarifying note near the top of the article.

      October 27th, 2015 at 01:39

  3. voracity

    Oh, I suppose this feature _would_ need a centralised server. There’s no decentralised alternative on the horizon (for an IPv6 world)?

    October 26th, 2015 at 19:57

  4. PhistucK

    Well, WebSockets require a constant connection to the server. Push does not require that, I believe and the service worker is generally not running, except when you use the website, or get a push notification. This saves a lot of battery use.

    October 27th, 2015 at 01:31

  5. Rafael

    Looks like Pinterest has been testing this out for a while but haven’t seen the actual notifications. I keep getting the allow Pinterest to send notifications but I guess that pref bit isn’t setting yet.

    Not bad.

    October 29th, 2015 at 12:47

  6. Tyf0x

    I understand that push is an alternative to websockets which doesn’t require a permanent connection between the scripts and the push server.

    But in order for the server to send anything to the client, a connection needs to be maintained somewhere (else we would have firewall/NAT issues).

    Does this mean that under the hood Firefox maintains an opened socket connection? If yes, is it saving resources because only one connection to the push server is required no matter how many scripts endpoints?

    Or am I totally mistaken?

    October 29th, 2015 at 22:59

    1. Chris Mills

      This is an interesting question; I’m not sure exactly what Fx does under the hood to maintain the connection, so I’ll reach out to our engineers to ask them for a proper answer.

      October 30th, 2015 at 05:51

      1. Chris Mills

        Ok, I’ve got an answer for you, from our push engineering team

        So, yes, Firefox maintains a connection to a single server. This single connection allows us to consolidate a lot of traffic into one connection, which provides opportunities to save battery and even networking costs. In future, we may be able to remove even
        that single connection and use more advanced techniques for delivering messages.

        Push isn’t always a complete or perfect replacement for websockets. If your application is an intense user of websockets, you will be better off establishing a websocket and using that for shorter periods of bidirectional message exchange. Push is best suited to small, infrequent messages. Push will never be as effective at full duplex communications as websockets, or as well-suited to operating at large scale as HTTP.

        In particular, push messaging will add extra latency in almost every case. If you care about latency, WebRTC data channels are the best option currently available.

        On the other hand, push is always better than a long-term websocket (and almost always better than long-polling).

        October 30th, 2015 at 06:27

  7. Tyf0x

    Thanks for digging the answer out :)

    This makes total sense.

    I’m now curious about this “more advanced technique” that they could be using in the future. I guess I’ll have to wait for it to be implemented, then ask the question.

    October 30th, 2015 at 07:14

Comments are closed for this article.