Push Notifications Using Node.js & Service Worker

Before we dive into building a push notification system, it's advisable that you have some basic knowledge of node.js, for service workers there are kinda difficult to grasp if you're just getting started with them, there are the core of progress web apps (PWA) here is a friendly introduction to service workers in the Medium website.

We're going to build this notification system using a nodejs module called web-push. For those who had used push notification before using services like Pusher or PubNub, this web-push module allows us to thesame thing without using those third party services.

What is a push notification?

A Push notifications is like SMS text messages and mobile alerts, but they only reach users who visited the website. Most browsers has support for push notifications. If a user visit your site on a browser that doesn't support push notifications it does no harm, the user just wouldn't see the notification and that's it.

Why use push notification?

  • Improving user experience
  • Capturing users attention
  • Utility messages like sales, offers or promotion if it's an e-commerce website
  • And many more..

Here is what it looks like

Prerequisite

Before you jump into your computer and start creating folder for the project, you'll need to have node.js installed on your machine. If you've not installed it already Here is the link

Setup

Generating the package.json file Run this command in your terminal, make sure you've cd into the folder you created for the project.

        
          npm init -y
        
      

After you've ran that command you should see a package.json file.

Next we want to install our dependencies, we need web-push, express & body-parser

        
          npm install web-push express body-parser
        
      

After there's all installed, go to your package.json file change the script from this

            
              "scripts":{
                "test": "echo \"Error: no test specified\" && exit 1"
              }
            
          

To this

            
              "scripts":{
                "start": "node index.js"
              }
            
          

Noticed that I used index.js as my entry file, but you can use what ever name you like as your entry JS file.

Requiring the modules

Create a js file in the root directory name it "the same name you used in the package.json file". Now go into the js file you just created and require all the modules we installed.

        
        const express = require("express");
        const webpush = require("web-push");
        const bodyParser = require("body-parser");

        //Path module
        const path = require("path");
        
      

initiallizing express

        
          const app = express();
        
      

We want to create a set of vapid keys, but first you'll need to specify the location in your terminal. In your terminal make sure you're in the root directory of the project you're working in. run this command and hit enter in your keyboard.

        
        ./node_modules/.bin/web-push generate-vapid-keys
        
      

After you've ran the command you should see two keys generated in the terminal (Public key: .......) & (Private key: .......). PLEASE DONT CLOSE THE TERMINAL YET

In your js file create two variables call one publicVapidKey and the other privateVapidKey. Like this:

        
        const publicVapidKey = "";
        const privateVapidKey = "";
        
      

In your terminal, copy the generated key from the Public & Private key and paste them in the variables you just created . Like this:

        
      const publicVapidKey = "BBZ9QzXuTWpafrqGx4Qxo3Q6Bu4RLIoxnv__-U11AvFumVsfBq6q3KTwqTjHIHUJSr1-vrSYA5xL5MIZfjmCwd4";
      const privateVapidKey = "YBoYT788kv-cV2vrV9I48Kii955WWnIhRYpubZQZGpc";
        
      

Set body-parser & the static path

At the bottom of where we initialized express() place this code.

        
    //set static path
     app.use(express.static(path.join(__dirname, "client")));

     //use body-parser
     app.use(bodyParser.json());
        
      

setVapidDetails

        
    webpush.setVapidDetails("mailto:test@gmail.com", publicVapidKey, privateVapidKey);
        
      

What the vapidkeys does is basicaly identifying who's sending the push notification.

Subscribe Route

This is resposnsible for sending the notification to the service workers

        
    app.post("/subscribe", (req, res) => {
       //get pushSubscription object
       const subscription = req.body;

       res.status(201).json({});

       //creat paylaod
       const paylaod = JSON.stringify({ title: "Notification From Node.js App" });

   //pass object into sendNotification
   webpush.sendNotification(subscription, paylaod).catch(err => console.error(err));

   });
        
      

Creating our server

        
        const port = 3000;

        app.listen(port, () => console.log(`Server started on port ${port}`));
        
      

Step 2

In your root directory create a folder, name it "client" remember when we set the express.static we use client as the folder name. so make sure you name the folder as the name you used in the path module.

In that client folder create a client.js, worker.js and index.html file.

The index.html page is the page you want the push notification to show on. For sake of this tutorial we're going to put a h1 tag inside the index.html page you can write whatever you like in the h1.

The client.js file

In the client.js we actually want that Public key, copy the publicVapidKey variable you created in the previous js file and paste it in the client.js file. Like this

        
   const publicVapidKey = "BBZ9QzXuTWpafrqGx4Qxo3Q6Bu4RLIoxnv__-U11AvFumVsfBq6q3KTwqTjHIHUJSr1-vrSYA5xL5MIZfjmCwd4";
        
      

Check for service worker

We are going to check to see if we are able to use service worker in the current browser. Note that we're now working in the client.js file.

        
    //check for service worker
    if("serviceWorker" in navigator){
        send().catch(err => console.error(err));
    }
        
      

Registering

We are going to register the service worker, the push notification and send the push notification. - outside the if() statement

        
async function send(){
    console.log("Registering service worker.");
    const register = await navigator.serviceWorker.register("/worker.js", {
        scope: "/"
    });

    console.log("Service worker registered");


    //Register Push
    console.log("Registering Push..");
    const subscription = await register.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array(publicVapidKey)
    });

    console.log("Push Registered..");

    //sending push
    await fetch("/subscribe", {
        method: 'POST',
        body: JSON.stringify(subscription),
        headers: {
            "content-type": "application/json"
        }
    });

    console.log("Push sent..");

}

        
      

Converting the URL safe base54 string to a Unit8Array

Outside of the async send() function

      
  function urlBase64ToUint8Array(base64String) {
    const padding = "=".repeat((4 - base64String.length % 4) % 4);
    const base64 = (base64String + padding)
      .replace(/\-/g, "+")
      .replace(/_/g, "/");

    const rawData = window.atob(base64);
    const outputArray = new Uint8Array(rawData.length);

    for (let i = 0; i < rawData.length; ++i) {
      outputArray[i] = rawData.charCodeAt(i);
    }
    return outputArray;
  }
      
    

The worker.js file

We are done with our client.js, now we procceed to the worker.js

Handling the push event

        
    self.addEventListener("push", e => {
        const data = e.data.json();
      self.registration.showNotification(data.title, {
          body: "Getting Started With Node.js: A beginners Guide - Anthonylan.com",
          icon: "https://cdn2.iconfinder.com/data/icons/nodejs-1/512/nodejs-512.png"
    });
    });
        
      

And that's it

Now you can run your app by running "npm start" in your terminal. Once the server has started you can now go to localhost:3000.

Recap

To make it less confusing here are the codes for the 3 JavaScript files we created. take a look to see if you missed something.

The index.js file, whatever you named it

        

      const express = require("express");
      const webpush = require("web-push");
      const bodyParser = require("body-parser");
      const path = require("path");


      const app = express();


      //set static path
      app.use(express.static(path.join(__dirname, "client")));

      //use body-parser
      app.use(bodyParser.json());

        const publicVapidKey = "BBZ9QzXuTWpafrqGx4Qxo3Q6Bu4RLIoxnv__-U11AvFumVsfBq6q3KTwqTjHIHUJSr1-vrSYA5xL5MIZfjmCwd4";
        const privateVapidKey = "YBoYT788kv-cV2vrV9I48Kii955WWnIhRYpubZQZGpc";

      webpush.setVapidDetails("mailto:test@gmail.com", publicVapidKey, privateVapidKey);

      //subscribe route

      app.post("/subscribe", (req, res) => {
          //get pushSubscription object
          const subscription = req.body;

          res.status(201).json({});

          //creat paylaod
          const paylaod = JSON.stringify({ title: "Notification From Node.js App" });

      //pass object into sendNotification
      webpush.sendNotification(subscription, paylaod).catch(err => console.error(err));

      });

      const port = 3000;

      app.listen(port, () => console.log(`Server started on port ${port}`));

        
      

The client.js file

        
     const publicVapidKey = "BBZ9QzXuTWpafrqGx4Qxo3Q6Bu4RLIoxnv__-U11AvFumVsfBq6q3KTwqTjHIHUJSr1-vrSYA5xL5MIZfjmCwd4";

    //check for service worker

    if("serviceWorker" in navigator){
        send().catch(err => console.error(err));

    }


    async function send(){
        console.log("Registering service worker.");
        const register = await navigator.serviceWorker.register("/worker.js", {
            scope: "/"
        });

        console.log("Service worker registered");


        //Register Push
        console.log("Registering Push..");
        const subscription = await register.pushManager.subscribe({
            userVisibleOnly: true,
            applicationServerKey: urlBase64ToUint8Array(publicVapidKey)
        });

        console.log("Push Registered..");

        //sending push
        await fetch("/subscribe", {
            method: 'POST',
            body: JSON.stringify(subscription),
            headers: {
                "content-type": "application/json"
            }
        });

        console.log("Push sent..");

    }


    function urlBase64ToUint8Array(base64String) {
        const padding = "=".repeat((4 - base64String.length % 4) % 4);
        const base64 = (base64String + padding)
          .replace(/\-/g, "+")
          .replace(/_/g, "/");

        const rawData = window.atob(base64);
        const outputArray = new Uint8Array(rawData.length);

        for (let i = 0; i < rawData.length; ++i) {
          outputArray[i] = rawData.charCodeAt(i);
        }
        return outputArray;
      }
          
      

The worker.js file

        
  self.addEventListener("push", e => {
      const data = e.data.json();
    self.registration.showNotification(data.title, {
        body: "Getting Started With Node.js: A beginners Guide - Anthonylan.com",
        icon: "https://cdn2.iconfinder.com/data/icons/nodejs-1/512/nodejs-512.png"
  });
  });