Mozilla's getting a new look. What do you think? https://mzl.la/brandsurvey

The W3C Push API offers some exciting new functionality for developers to use in web applications: this article provides an introduction to getting Push notifications setup and running, with a simple demo.

The ability to push messages or notifications from a server to a client at any time—whether the app is active on your system or not—is something that has been enjoyed by native platforms for some time, and it is finally coming to the Web! Support for most of Push is now available in Firefox 43+ and Chrome 42+ on desktop, with mobile platforms hopefully following soon. PushMessageData is currently only supported experimentally in Firefox Nightly (44+), and the implementation is subject to change.

Note: Early versions of Firefox OS used a proprietary version of this API called Simple Push. This is being rendered obsolete by the Push API standard.

Demo: the basis of a simple chat server app

The demo we've created provides the beginnings of a simple chat app. It presents a form for you to enter your chat handle into and a button to press to subscribe to push messaging. Once the button is pressed, you are subscribed to push messaging, your details are recorded on the server, and a push message is sent that tells all current subscribers that someone has subscribed.

At this point, the new subscriber's name will appear in the subscriber's list, along with a text field and submit button to allow the subscriber to send messages.

To run the demo, follow the instructions on the push-api-demo README. Note that the server-side component still needs a bit of work to make it run in Chrome and generally run in a more reasonable way. But the Push aspects can still be explained thoroughly; we'll dive into it after we review the technologies at play.

Technology overview

This section provides an outline as to what technologies are involved in this example.

Web Push messages are part of the service workers technology family; in particular, a service worker is required to be active on the page for it to receive push messages. The service worker receives the push message, and then it is up to you how to then notify the page. You can:

  • Send a Web notification to pop up a system notification to alert the user. This requires permission to be granted for sending push messages.
  • Send a message back to the main page via a MessageChannel.

Often a combination of the two will be required; the demo below features an example of each.

Note: You need some form of code running on the server to handle the endpoint/data encryption and send push message requests. In our demo we have put together a quick-and-dirty server using NodeJS.

The service worker also has to subscribe to the push messaging service. Each session is given its own unique endpoint when it subscribes to the push messaging service. This endpoint is obtained from the (PushSubscription.endpoint) property on the subscription object. This endpoint can be sent to your server and used to send a message to that session's active service worker. Each browser has its own push messaging server to handle sending the push message.

Encryption

Note: For an interactive walkthrough, try JR Conlin's Web Push Data Encryption Test Page.

To send data via a push message, it needs to be encrypted. This requires a public key created using the PushSubscription.getKey() method, which relies upon some complex encryption mechanisms that are run server-side; read Message Encryption for Web Push for more details. As time goes on, libraries will appear to handle key generation and encryption/decryption of push messages; for this demo we used Marco Castelluccio's NodeJS web-push library.

Note: There is also another library to handle the encryption with a Node and Python version available, see encrypted-content-encoding.

Push workflow summary

To summarize, here is what is needed to implement push messaging. You can find more details about specific parts of the demo code in subsequent sections.

  1. Request permission for web notifications, or anything else you are using that requires permissions.
  2. Register a service worker to control the page by calling ServiceWorkerContainer.register().
  3. Subscribe to the push messaging service using PushManager.subscribe().
  4. Retrieve the endpoint associated with the subscription and generate a client public key (PushSubscription.endpoint and PushSubscription.getKey(). Note that getKey() is currently experimental and Firefox only.)
  5. Send these details to the server so it can send push message when required. This demo uses XMLHttpRequest, but you could use Fetch.
  6. If you are using the Channel Messaging API to comunicate with the service worker, set up a new message channel (MessageChannel.MessageChannel()) and send port2 over to the service worker by calling Worker.postMessage() on the service worker, in order to open up the communication channel. You should also set up a listener to respond to messages sent back from the service worker.
  7. On the server side, store the endpoint and any other required details so they are available when a push message needs to be sent to a push subscriber (we are using a simple text file, but you could use a database or whatever you like). In a production app, make sure you keep these details hidden, so malicious parties can't steal endpoints and spam subscribers with push messages.
  8. To send a push message, you need to send an HTTP POST to the endpoint URL. The request must include a TTL header that limits how long the message should be queued if the user is not online. To include payload data in your request, you must encrypt it (which involves the client public key). In our demo, we are using the web-push module, which handles all the hard work for you.
  9. Over in your service worker, set up a push event handler to respond to push messages being received.
    1. If you want to respond by sending a channel message back to the main context (see Step 6) you need to first get a reference to the port2 we sent over to the service worker context (MessagePort). This is available on the MessageEvent object passed to the onmessage handler (ServiceWorkerGlobalScope.onmessage). Specifically, this is found in the ports property, index 0. Once this is done, you can send a message back to port1, using MessagePort.postMessage().
    2. If you want to respond by firing a system notification, you can do this by calling ServiceWorkerRegistration.showNotification(). Note that in our code 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 that we want to happen.

Building up the demo

Let's walk through the code for the demo so we can start to understand how this all works.

The HTML and CSS

There is nothing remarkable about the HTML and CSS for the demo; the HTML initially contains a simple form to allow you to enter your handle for the chat room, a button to click to subscribe to push notifications, and two lists into which subscribers and chat messages will be placed. Once subscribed, additional controls appear to allow the user to actually type in chat messages.

The CSS has been kept very minimal so as not to detract from the explanation of the Push API functionality.

The main JavaScript file

The JavaScript is obviously a lot more substantial. Let's take a look at the main JavaScript file.

Variables and initial setup

To start with, we declare some variables to use in our app:

var isPushEnabled = false;
var useNotifications = false;

var subBtn = document.querySelector('.subscribe');
var sendBtn;
var sendInput;

var controlsBlock = document.querySelector('.controls');
var subscribersList = document.querySelector('.subscribers ul');
var messagesList = document.querySelector('.messages ul');

var nameForm = document.querySelector('#form');
var nameInput = document.querySelector('#name-input');
nameForm.onsubmit = function(e) {
  e.preventDefault()
};
nameInput.value = 'Bob';

First, we have two booleans to track whether push is subscribed, and whether notification permission has been granted.

Next, we grab a reference to the subscribe/unsubscribe <button>, and declare variables to store references to our message sending button/input (which are only created when subscription is successful.)

The next variables grab references to the three main <div> elements in the layout, so we can insert elements into those (for example when the Send Chat Message button appears, or a chat message appears in the Messages list.)

Finally we grab references to our name selection form and <input> element, give the input a default value, and use preventDefault() to stop the form submitting when the form is submitted by pressing return.

Next, we request permission to send web notifications, using requestPermission():

Notification.requestPermission();

Now we run a section of code when onload is fired, to start up the process of inialising the app when it is first loaded. First of all we add a click event listener to the subscribe/unsubscribe button that runs our unsubscribe() function if we are already subscribed (isPushEnabled is true), and subscribe() otherwise:

window.addEventListener('load', function() {   
  subBtn.addEventListener('click', function() {  
    if (isPushEnabled) {  
      unsubscribe();  
    } else {  
      subscribe();  
    }  
  });

Next we check to see if service workers are supported. If so, we register a service worker using ServiceWorkerContainer.register(), and run our initialiseState() function. If not, we deliver an error message to the console.

  // Check that service workers are supported, if so, progressively  
  // enhance and add push messaging support, otherwise continue without it.  
  if ('serviceWorker' in navigator) {  
    navigator.serviceWorker.register('sw.js').then(function(reg) {
      if(reg.installing) {
        console.log('Service worker installing');
      } else if(reg.waiting) {
        console.log('Service worker installed');
      } else if(reg.active) {
        console.log('Service worker active');
      }

      initialiseState(reg);
    });  
  } else {  
    console.log('Service workers aren\'t supported in this browser.');  
  }  
});

The next thing in the source code is the initialiseState() function — for the full commented code, look at the initialiseState() source on Github (we are not repeating it here for brevity's sake.)

initialiseState() first checks whether notifications are supported on service workers, then sets the useNotifications variable to true if so. Next, it checks whether said notifications are permitted by the user, and if push messages are supported, and reacts accordingly to each.

Finally, it uses ServiceWorkerContainer.ready() to wait until the service worker is active and ready to start doing things. Once its promise resolves, we retrieve our subscription to push messaging using the ServiceWorkerRegistration.pushManager property, which returns a PushManager object that we then call PushManager.getSubscription() on. Once this second inner promise resolves, we enable the subscribe/unsubscribe button (subBtn.disabled = false;), and check that we have a subscription object to work with.

If we do, then we are already subscribed. This is possible when the app is not open in the browser; the service worker can still be active in the background. If we're subscribed, we update the UI to show that we are subscribed by updating the button label, then we set isPushEnabled to true, grab the subscription endpoint from PushSubscription.endpoint, generate a public key using PushSubscription.getKey(), and run our updateStatus() function, which as you'll see later communicates with the server.

As an added bonus, we set up a new MessageChannel using the MessageChannel.MessageChannel() constructor, grab a reference to the active service worker using ServiceworkerRegistration.active, then set up a channel betweeen the main browser context and the service worker context using Worker.postMessage(). The browser context receives messages on MessageChannel.port1; whenever that happens, we run the handleChannelMessage() function to decide what to do with that data (see the Handling channel messages sent from the service worker section).

Subscribing and unsubscribing

Let's now turn our attention to the subscribe() and unsubscribe() functions used to subscribe/unsubscribe to the push notification service.

In the case of subscription, we again check that our service worker is active and ready by calling ServiceWorkerContainer.ready(). When the promise resolves, we subscribe to the service using PushManager.subscribe(). If the subscription is successful, we get a PushSubscription object, extract the subscription endpoint from this and generate a public key (again, PushSubscription.endpoint and PushSubscription.getKey()), and pass them to our updateStatus() function along with the update type (subscribe) to send the necessary details to the server.

We also make the necessary updates to the app state (set isPushEnabled to true) and UI (enable the subscribe/unsubscribe button and set its label text to show that the next time it is pressed it will unsubscribe.)

The unsubscribe() function is pretty similar in structure, but it basically does the opposite; the most notable difference is that it gets the current subscription using PushManager.getSubscription(), and when that promise resolves it unsubscribes using PushSubscription.unsubscribe().

Appropriate error handling is also provided in both functions.  

We only show the subscribe() code below, for brevity; see the full subscribe/unsubscribe code on Github.

function subscribe() {
  // Disable the button so it can't be changed while
  // we process the permission request

  subBtn.disabled = true;

  navigator.serviceWorker.ready.then(function(reg) {
    reg.pushManager.subscribe({userVisibleOnly: true})
      .then(function(subscription) {
        // The subscription was successful
        isPushEnabled = true;
        subBtn.textContent = 'Unsubscribe from Push Messaging';
        subBtn.disabled = false;
        
        // Update status to subscribe current user on server, and to let
        // other users know this user has subscribed
        var endpoint = subscription.endpoint;
        var key = subscription.getKey('p256dh');
        updateStatus(endpoint,key,'subscribe');
      })
      .catch(function(e) {
        if (Notification.permission === 'denied') {
          // The user denied the notification permission which
          // means we failed to subscribe and the user will need
          // to manually change the notification permission to
          // subscribe to push messages
          console.log('Permission for Notifications was denied');
          
        } else {
          // A problem occurred with the subscription, this can
          // often be down to an issue or lack of the gcm_sender_id
          // and / or gcm_user_visible_only
          console.log('Unable to subscribe to push.', e);
          subBtn.disabled = false;
          subBtn.textContent = 'Subscribe to Push Messaging';
        }
      });
  });
}

Updating the status in the app and server

The next function in our main JavaScript is updateStatus(), which updates the UI for sending chat messages when subscribing/unsubscribing and sends a request to update this information on the server.

The function does one of three different things, depending on the value of the statusType parameter passed into it:

  • subscribe: The button and text input for sending chat messages are created and inserted into the UI, and an object is sent to the server via XHR containing the status type (subscribe), username of the subscriber, subscription endpoint, and client public key.
  • unsubscribe: This basically works in the opposite way to subscribe — the chat UI elements are removed, and an object is sent to the server to tell it that the user has unsubscribed.
  • init: This is run when the app is first loaded/initialised — it creates the chat UI elements, and sends an object to the server to tell it that which user has reinitialised (reloaded.)

Again, we have not included the entire function listing for brevity. Examine the full updateStatus() code on Github.

Handling channel messages sent from the service worker

As mentioned earlier, when a channel message is received from the service worker, our handleChannelMessage() function is called to handle it. This is done by our handler for the message event, channel.port1.onmessage:

channel.port1.onmessage = function(e) {
  handleChannelMessage(e.data);
}

This occurs when the service worker sends a channel message over.

The handleChannelMessage() function looks like this:

function handleChannelMessage(data) {
  if(data.action === 'subscribe' || data.action === 'init') {
    var listItem = document.createElement('li');
    listItem.textContent = data.name;
    subscribersList.appendChild(listItem);
  } else if(data.action === 'unsubscribe') {
    for(i = 0; i < subscribersList.children.length; i++) {
      if(subscribersList.children[i].textContent === data.name) {
        subscribersList.children[i].parentNode.removeChild(subscribersList.children[i]);
      }
    }
    nameInput.disabled = false;
  } else if(data.action === 'chatMsg') {
    var listItem = document.createElement('li');
    listItem.textContent = data.name + ": " + data.msg;
    messagesList.appendChild(listItem);
    sendInput.value = '';
  }
}

What happens here depends on what the action property on the data object is set to:

  • subscribe or init (at both startup and restart, we need to do the same thing in this sample): An <li> element is created, its text content is set to data.name (the name of the subscriber), and it is appended to the subscribers list (a simple <ul> element) so there is visual feedback that a subscriber has (re)joined the chat.
  • unsubscribe: We loop through the children of the subscribers list, find the one whose text content is equal to data.name (the name of the unsubscriber), and delete that node to provide visual feedback that someone has unsubscribed.
  • chatMsg: In a similar manner to the first case, an <li> element is created, its text content is set to data.name + ": " + data.msg (so for example "Chris: This is my message"), and it is appended to the chat messages list; this is how the chat messages appear on the UI for each user.

Note: We have to pass the data back to the main context before we do DOM updates because service workers don't have access to the DOM. You should be aware of the limitations of service workers before attemping to ue them. Read Using Service Workers for more details.

Sending chat messages

When the Send Chat Message button is clicked, the content of the associated text field is sent as a chat message. This is handled by the sendChatMessage() function (again, not shown in full for brevity). This works in a similar way to the different parts of the updateStatus() function (see Updating the status in the app and server) — we retrieve an endpoint and public key via a PushSubscription object, which is itself retrieved via ServiceWorkerContainer.ready() and PushManager.subscribe(). These are sent to the server via XMLHttpRequest in a message object, along with the name of the subscribed user, the chat message to send, and a statusType of chatMsg.

The server

As mentioned above, we need a server-side component in our app, to handle storing subscription details, and send out push messages when updates occur. We've hacked together a quick-and-dirty server using NodeJS (server.js), which handles the XHR requests sent by our client-side JavaScript code.

It uses a text file (endpoint.txt) to store subscription details; this file starts out empty. There are four different types of request, marked by the statusType property of the object sent over in the request; these are the same as those understood client-side, and perform the required server actions for that same situation. Here's what each means in the context of the server:

  • subscribe: The server adds the new subscriber's details into the subscription data store (endpoint.txt), including the endpoint, and then sends a push message to all the endpoints it has stored to tell each subscriber that someone new has joined the chat.
  • unsubscribe: The server finds the sending subscriber's details in the subscription store and removes it, then sends a push message to all remaining subscribers telling them the user has unsubscribed.
  • init: The server reads all the current subscribers from the text file, and sends each one a push message to tell them a user has initialized (rejoined) the chat.
  • chatMsg: Sent by a subscriber that wishes to deliver a message to all users; the server reads the list of all current subscribers from the subscription store file, then sends each one a push message containing the new chat message they should display.

A couple more things to note:

  • We are using the Node.js https module to create the server, because for security purposes, service workers only work on a secure connection. This is why we need to include the .pfx security cert in the app, and reference it when creating the server in the Node code.
  • When you send a push message without data, you simply send it to the endpoint URL using an HTTP POST request. However, when the push message contains data, you need to encrypt it, which is quite a complex process. As time goes on, libraries will appear to do this kind of thing for you; for this demo we used Marco Castelluccio's NodeJS web-push library. Have a look at the source code to get more of an idea of how the encryption is done (and read Message Encryption for Web Push for more details.) The library makes sending a push message simple.

The service worker

Now let's have a look at the service worker code (sw.js), which responds to the push messages, represented by push events. These are handled on the service worker's scope by the (ServiceWorkerGlobalScope.onpush) event handler; its job is to work out what to do in response to each received message. We first convert the received message back into an object by calling PushMessageData.json(). Next, we check what type of push message it is, by looking at the object's action property:

  • subscribe or unsubscribe: We send a system notification via the fireNotification() function, but also send a message back to the main context on our MessageChannel so we can update the subscriber list accordingly (see Handling channel messages sent from the service worker for more details).
  • init or chatMsg: We just send a channel message back to the main context to handle the init and chatMsg cases (these don't need a system notification).
self.addEventListener('push', function(event) {
  var obj = event.data.json();

  if(obj.action === 'subscribe' || obj.action === 'unsubscribe') {
    fireNotification(obj, event);
    port.postMessage(obj);
  } else if(obj.action === 'init' || obj.action === 'chatMsg') {
    port.postMessage(obj);
  }
});

Next, let's look at the fireNotification() function (which is blissfully pretty simple).

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  
  }));
}

Here we assemble the assets needed by the notification box: the title, body, and icon. Then we send a notification via the ServiceWorkerRegistration.showNotification() method, providing that information as well as the tag "push", which we can use to identify this notification among any other notifications we might be using. When the notification is successfully sent, it manifests as a system notification dialog on the users computers/devices in whatever style system notifications look like on those systems (the following image shows a Mac OSX system notification.)

Note that we do this from inside an ExtendableEvent.waitUntil() method; this is to make sure the service worker remains active until the notification has been sent. waitUntil() will extend the life cycle of the service worker until everything inside this method has completed.

Note: Web notifications from service workers were introduced around Firefox version 42, but are likely to be removed again while the surrounding functionality (such as Clients.openWindow()) is properly implemented (see bug 1203324 for more details.)

Handling premature subscription expiration

Sometimes push subscriptions expire prematurely, without PushSubscription.unsubscribe() being called. This can happen when the server gets overloaded, or if you are offline for a long time, for example.  This is highly server-dependent, so the exact behavior is difficult to predict. In any case, you can handle this problem by watching for the pushsubscriptionchange event, which you can listen for by providing a ServiceWorkerGlobalScope.onpushsubscriptionchange event handler; this event is fired only in this specific case.

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

Note that we don't cover this case in our demo, as a subscription ending is not a big deal for a simple chat server. But for a more complex example you'd probably want to resubscribe the user.

Extra steps for Chrome support

To get the app working on Chrome, we need a few extra steps, as Chrome currently relies on Google's Cloud Messaging service to work.

Setting up Google Cloud Messaging

To get this set up, follow these steps:

  1. Navigate to the Google Developers Console  and set up a new project.
  2. Go to your project's homepage (ours is at https://console.developers.google.com/project/push-project-978, for example), then
    1. Select the Enable Google APIs for use in your apps option.
    2. In the next screen, click Cloud Messaging for Android under the Mobile APIs section.
    3. Click the Enable API button.
  3. Now you need to make a note of your project number and API key because you'll need them later. To find them:
    1. Project number: click Home on the left; the project number is clearly marked at the top of your project's home page.
    2. API key: click Credentials on the left hand menu; the API key can be found on that screen.

manifest.json

You need to include a Google app-style manifest.json file in your app, which references the project number you made a note of earlier in the gcm_sender_id parameter. Here is our simple example manifest.json:

{  
  "name": "Push Demo",  
  "short_name": "Push Demo",  
  "icons": [{  
        "src": "push-icon.png",  
        "sizes": "111x111",
        "type": "image/png"
      }],  
  "start_url": "/index.html",  
  "display": "standalone",  
  "gcm_sender_id": "224273183921"    
}

You also need to reference your manifest using a <link> element in your HTML:

<link rel="manifest" href="manifest.json">

userVisibleOnly

Chrome requires you to set the userVisibleOnly parameter to true when subscribing to the push service, which indicates that we are promising to show a notification whenever a push is received. This can be seen in action in our subscribe() function.

See also

Note: Some of the client-side code in our Push demo is heavily influenced by Matt Gaunt's excellent examples in Push Notifications on the Open Web. Thanks for the awesome work, Matt!

Document Tags and Contributors

 Contributors to this page: freakypie, chrisdavidmills, Callahad, kitcambridge, gajus, Sheppy, m_t, marco-c
 Last updated by: freakypie,