Beginners guide to Web Push Notifications using Service Workers
https://blog.atulr.com/web-notifications/
Last updated
https://blog.atulr.com/web-notifications/
Last updated
September 25, 2018
Push notifications are very common in the native mobile application platforms like Android & iOS. The are most effective ways to re-engage users to your apps. In this post we will look at how to implement push notifications for the web.
Notifications can be of two types:
Local Notification: This is generated by your app itself.
Push Notification: This is generated by a server via a push event.
If you are here only for Push notifications, you can safely skip to Step 3: Push Notification
Components needed for Push Notification:
Notification API: This API is used to show a notification prompt to the user just like in case of mobile devices.
Push API: This API is used to get the push message from the server.
The steps involved in making a local push notification:
Ask for permission from the user using the Notification APIs requestPermission method.
If permission granted :
Brilliant! Now we use our service worker to subscribe to a Push Service using Push API.
Our Service Worker now listens for push events.
On arrival of a push event, service worker awakens and uses the information from the push message to show a notification using notification API.
If permission denied : There is nothing much you can do here. So make sure you handle this case in code as well.
The entire code for this blog is available at https://github.com/a7ul/web-push-demo
Lets create a basic web app (no frameworks).
Now we have a basic project structure. Lets add some basic code.
index.html
index.js
index.css
To run this project locally, lets use a simple development server. I will be using http-server
npm module.
Inside the frontend directory run
Lets check support in our code. Change the contents of index.js to: index.js
If the output in the browser console doesn’t show any errors at this point, you are good to go.
To register a service worker that runs in background: First we add a new js file service.js : This will contain all our service worker code that will run on a separate thread.
service.js
Now modify the frontend index.js to:
Since service workers are registered only once, If you refresh the app using browser refresh button you will not see Hello from service worker
again.
You can call register() every time a page loads without concern; the browser will figure out if the service worker is already registered or not and handle it accordingly.
As service workers are event oriented, if we need to do something useful with service workers we will need to listen to events inside it.
One subtlety with the register() method is the location of the service worker file. You’ll notice in this case that the service worker file is at the root of the domain. This means that the service worker’s scope will be the entire origin. In other words, this service worker will receive fetch events for everything on this domain. If we register the service worker file at /example/sw.js, then the service worker would only see fetch events for pages whose URL starts with /example/ (i.e. /example/page1/, /example/page2/).
Debugging Tip for Service workers
If you are using Chrome dev tools: You can go to Application Tab > Service Workers. Here you can unregister the service worker and refresh the app again. For debugging purposes, I would suggest you enable update on reload checkbox on the top to avoid manual unregister every time you change the service worker file. More detailed guide here.
If you are using Firefox: On the Menu bar go to Tools > Web Developer > Service workers or type in the URL bar: about:debugging#workers. This should show you list of all service workers.
Permission for Notification
In order to show a web notification to the user, the web app needs to get permission from the user. Modify the frontend index.js to:
Note: We are asking for notification permission in the main function since this is a demo app. But ideally, we should not be doing it here as it accounts for bad UX. More details on where you should be calling it in a production app here.
An alternative approach here would be to add a button asking the user to subscribe to notifications and then on click of that button we show the notification prompt.
You can also get the value of permission via
This can be used to hide the button if the user has already answered the notification prompt (if the value is not ‘default’).
Now that we have the permission from the user. We can go ahead and try out the notification popup. To do that modify frontend index.js
Possible values of Notification Option:
Details of each and every property is listed here
Main thread: The main JS thread that runs when we are browsing a web page with javascript. Service Worker thread: This is an independent javascript thread which runs on the background and can run even when the page has been closed.
If you notice we havent used our service worker till now for any purpose other than just registering it. With respect to Push Notifications, the JS main thread should only be used for all UI related stuff like changing DOM elements or asking a user, the permissions for showing notifications. Notice here that I am displaying the notification from the main thread (index.js). Ideally we will not do this in the main thread. Keep in mind that notifications are only useful for re engaging the user back to your app (ie when the page is not open). If you show notification when the user is on your page, then the user actually gets distracted.
The worker thread(service worker) should be used for waiting for push messages/events and showing the notifications. This is because it is not necessary that the actual page whould be open on the browser at the time a push event arrives (meaning main js thread doesnt exist in this case).
Hence, In practical usecases we will call showNotification from the service.js file. We will do that when we implement remote notifications. Here I just wanted to show you how the showNotification call looks like.
We subscribe our service worker to push events from the Push Service of browser.
We will send a message to the push service from our backend.
Push event from push service containing the message from our backend will arrive at the browser.
We use message from the push service to show the notification.
A push service receives a network request, validates it and delivers a push message to the appropriate browser. If the browser is offline, the message is queued until the the browser comes online.
Each browser can use any push service they want, it’s something developers have no control over. This isn’t a problem because every push service expects the same API call. Meaning you don’t have to care who the push service is. You just need to make sure that your API call is valid.
To know more: Read up here
Lets clean up our code a bit as per recommendations above. Make sure our files look like this now:
index.html
index.js
service.js
Lets run this: You should now see a button on the page saying ‘Ask Permission’. Clicking on it would call our main function.
We will now only focus on the service worker file from now on unless specified otherwise. To listen/subscribe to remote push messages we need the browser web app to register to the push service.
service.js
Now, unregister the service workers and refresh the web app. Click on Ask for permission button.
In Firefox console you should see:
The value of subscription will be null if the web app has not subscribed to the push service.
In Chrome console you would see an error:
This is because, there are two option parameters applicationServerKey (also known as VAPID public key) and userVisibleOnly which are optional in firefox but are required parameters in chrome.
userVisibleOnly: You need to send userVisibleOnly as true always in chrome. This parameter restricts the developer to use push messages for notifications. That is the developer will not be able use the push messages to send data to server silently without showing a notification. Currently it has to be set to true otherwise you get a permission denied error. In essence, silent push messages are not supported at the moment due to privacy and security concerns.
VAPID: Voluntary Application Server Identification for Web Push is a spec that allows a backend server(your application server) to identify itself to the Push Service(browser specific service). It is a security measure that prevents anyone else from sending messages to an application’s users.
How does VAPID keys enable security ?
In short:
You generate a set of private and public keys (VAPID keys) at the application server(for example:your backend NodeJS server).
Public key will be sent to the push service when the frontend app tries to subscribe to the push service. Now the push service knows the public key from your application server(backend). The subscribe method will return a unique endpoint for you frontend web app. You will store this unique endpoint at the backend application server.
From the application server, you will hit an API request to the unique push service endpoint that you just saved. In this API request you will sign some information in the Authorization header with the private key. This allows the push service to verify that the request came from the correct application server.
After validating the header, the push service will send the message to the frontend web app.
PS: To get the value of push subscription you can also call (in your service worker file)
Generating VAPID Keys
To generate a set of private and public VAPID keys:
This should print out something like this:
DO NOT USE THE ABOVE KEYS, GENERATE YOUR OWN.
You need to only generate this once. Keep your private key safe. Public key can be stored on frontend web app.
Now lets get back to service.js
Now refresh and unregister/re-register the service worker. Click on “Ask Permission”. You should see in your console.
This is quite similar to the one we received from the firefox subscription as well. Notice that fcm.googleapis.com (firebase) is the push service used by google chrome while firefox uses updates.push.services.mozilla.com . The Web Push Protocol standardises the Push API. This enables each browser to use the push service it deems fit as long as it conforms to the same API as mentioned in the spec.
Now once we receive the value of subscription, we need to save it in our backend server(application server). To do so we should create an API end point on our application server that will take in the subscription and store it in the database/cache. Lets for now consider that the API to hit is POST http://localhost:3000/save-subscription with request body containing the entire subscription object. We will make this API on the backend server soon.
So lets change the service.js file to add functionality to save the subscription.
Next step is to save subscription at the backend and then use the subscription to send push messages to the frontend app via the push service. But before that we will also need to listen to the push event from the backend on our frontend app.
Listen to Push event
In your service.js add
Time for backend (NodeJS)
Lets create a basic express js project.
Now create a backend index.js file
Run the node server as shown below and open up localhost:4000
on browser
Saving subscription on the backend Lets create our save-subscription endpoint on the express server. All we need to do is retrieve the subscription from frontend and stored it somewhere safely in the backend. We will need this subscription later to send push messages to the user’s browser.
Make changes to your backend nodejs index.js file :
Now we have saved the subscription at the backend, next step is to finally send a push message to the frontend.
To generate a push message we will use a npm library web-push which will help us encrypt the message, setting the correct parameters, legacy support for browsers etc. If you need to figure out how to do this in languages other than Javascript(NodeJS), you can take a look at the web-push source code.
In your backend project, install the npm module web-push locally.
In the backend index.js add the following:
Explanation webpush.setVapidDetails takes three arguments.
First argument needs to be either a URL or a mailto email address.
Second argument is the VAPID public key we generated earlier.
Third argument is the VAPID private key.
To test this out lets add another route /send-notification to our NodeJS express server. The complete backend index.js file would look something like this.
Lets try it out!!
Lets open up http://127.0.0.1:8080 in the browser
Make sure you start from clean slate. Unregister service workers, clear up cache, etc. Now hit ask permission and you should see a console message success. Which means our push subscription has been saved in backend.
Now open up the backend route http://localhost:4000/send-notification in another tab in your browser (since it is a GET API). You should see hello world push message on the console of the frontend app.
Open up your frontend service.js and add the following:
The final service.js file looks like this:
Now lets run it one last time. Again, unregister everything, clean up cache and re open the browser tab. Do the same procedure as before.
Great article by Rich Harris: Stuff I wish I’d known sooner about service workers https://gist.github.com/Rich-Harris/fd6c3c73e6e707e312d7c5d7d0f3b2f9
The entire code for this blog is available at https://github.com/a7ul/web-push-demo
Now open up http://127.0.0.1:8080/
on browser, you should get:
To make push notifications work we need Browser Push API and Service Workers. Almost all browsers support Service Workers. Browser support for Push API is also good. Except Safari (Has custom implementation of Push API), all major browsers support it.
Lets run and see:
Lets refresh and see what happens.
You should see something like this:
Wohoo !! 🚀 Now you can create web apps that can spam people too 🤣