As anyone who works for a US based company but lives in the UK knows, Thanksgiving is a wonderful time of the year. It is that point in the year when we can actually get work done without a barrage of emails hitting us in the morning and in the evening.

This Thanksgiving free-time I wanted to knock a project off my to-do list that had been sitting around for a while: a generic web-push web-hook end point.

The idea came about because I am very keen on the idea of the headless web — we can use experiences that are powered by “the web”, written in JavaScript and hosted on a site that requires no visible web browser to have a longer term relationship with. I also wanted to make incredibly easy to get notifications that you want to receive into your browser as quickly as possible without having to wait for a service to integrate them.

I would like to do this all without installing any application and the easiest way that I can see how to do it is to have my own URL endpoint that can be pinged whenever there is an action that is important to me.

I also wanted to create this as inspiration for any one else who wants to build this kind of service. I would be happy if IFTTT implemented web-push and then I could do away with this service :)

So, I created a simple example web app that if you execute the following curl command

Obviously, you could abuse this pretty easily and send me filthy things, but there are two interesting things 1) the end-point is unique to my instance of the browser and the service worker that is running and unless you give it to someone (like I have just done) only you can send the messages and 2) I have just killed this subscription, so the curl request would still succeed but you won’t know if it ever got through.

I like this service idea because it allows me to have push notifications for any service that I can to integrate with and that service doesn’t have to support web push.

I do expect to need to take this service down at some point and I would be happy when all the service I use use web push.

Implementing web push can seem like a daunting task, but I believe it is not too complicated and I hope to use this post to show you the code and some of my reasoning so that you can go and integrate web-push in to your own services.

Designing the Service

I’ve been having some luck with the nano instance on Compute Engine, it’s about 5USD per month and I know these services aren’t going to be chocker so it has been pretty inexpensive to run so far.

The rough architecture was based around 3 components that I had in my head:

The front-end server — displays the UI and creates the subscription for the user

The subscription service — saves the subscription to a data store

The send service — sends the message to the user

The Front-end Client

The idea is that as far as possible this front-end is stateless, the service that hosts it doesn’t care about the user and the only state that it needs is the Application Key that is used by the web push API (more on that later).

It’s sole job is to render a button, manage the subscription of the user to the push endpoint and then display a URL: The user clicks subscribe, they get their unique REST endpoint and a sample cUrl command that they can execute to test that it works.

A quick aside for anyone doing anything with web push in Chrome: If you call subscribe on a page that is already managing a subscription then you won’t be able to use the subscription again from the server. To fix it, I call unsubscribe before subscribing again.

The flow for the entire site is:

If there is a subscription, unsubscribe the user

Subscribe the user to push (at this point they would need to accept the notification permission) - in here we send the application server key (more on that later)

The Front-end Service Worker

The service worker is relatively lightweight. It receives a push message and displays it. It will also handle what happens when a user clicks on the notification and if a URL has been passed along with the payload then it will open that URL.

The Front-end Server

Generates the application server key each time it is booted up and embeds it in the web page. The application server key is stored and used in later in the process of signing and sending the push messages to the Push Message service — it is used to prove that send from the server is the actual sender of the message to the push service.

The front-end has two main endpoints:

/subscribe — takes the subscription object and puts it on to a PubSub queue so that a service can persist it.

/send — takes what ever curl or other input and passes it through to the subscription service

and that is all.

It is argueable if this service is needed because persistence could be done on the front-end and that would be ok. I made it a service because it enables me to keep huge swathes of logic out of the front-end, I don’t have to deal with the data store, it allows me to scale it horizontally and also scale logic out into different services as needed (logging service for instance) and chain them together (I worry about the whole YAGNI principle here, but I was having fun).

The Send Service

The send service is actually where the magic happens as it does the actual sending of the message. There is a lot of other infrastructure here that we have no control over (all of the GCM part), but the goal of this is to take a message, encrypt it ready for sending and then send it to the user.

The process is not too complex, we:

Wait to be told that a message needs to be sent, by listening to the send queue.

De-serialize the message

Find out the key for the user (from the data store — I am debating about pulling this out into the front-end) - this is needed for the VAPID process later.

Set up the VAPID headers

Send the notification to the endpoint the users for the browser the user is using.

The interesting thing is the interplay around the Push service. I heavily relied on the awesome Node Web-Push library to do all the grunt work of encrypting the payload that is being sent to the user.

Our article on sending push notifications describes how to encode the data to the push service and I would encourage you all to read it. The process is complex, but it is needed. The push service that the browser users should never be able to see the messages in between the service and the user, so that means we need to encrypt and sign the payload to ensure that it is not tampered with or inspected.