How to Add Web Push Notifications to Your Web App
Tags: javascript, webdev, programming, node
Published: 2023-05-03 20:44:40

Sending push notifications is a great way to communicate with your site's users even when they are not currently using your site. We get push notifications all the time from various apps on our phones such as Twitter, Instagram, and the Mail app.

It's a great time to start using web push notifications as it's now supported in all major browsers—including iOS devices, which was recently supported in iOS 16.4.

In this article, I'm going to show you how to use web push notifications by building an example from scratch.

Before we dive in, it's worth mentioning that using push notifications inappropriately can be annoying to users and considered a spam. These notifications should be sent for important updates that are timely, relevant, and precise. A good example is notifying the user when they receive a message to their inbox. A bad example is spamming the user with irrelevant ads and discounts, etc.

## What is a push notification?

Web push notifications are the combination of two different technologies: **Push Messages** and **Notifications**.

A notification is the notification card you see on your phone's home screen or on your desktop, at the top-right corner of your screen.

![Desktop notification](https://tahazsh.com/images/post-images/web-push-notifications-1.png)
![Mobile notification](https://tahazsh.com/images/post-images/web-push-notifications-2.png)

A push message, on the other hand, is the technology for sending a message from a server to a client. It's called **push** because it pushes the message without the client asking for it—which is the opposite of the traditional request/response workflow.

So when I say push notification, I mean the workflow of pushing a message from the server to the client and then display it on the client side using the notification card component.

## A general overview of the web push notification workflow

There are 6 main steps for sending a push notification:

1. Request permission from the user to display notifications.
2. Subscribe the user to your push notification service. This step gets you an object called *PushSubscription*. This object is used to identify which client to send the notification to. Each client has a different PushSubscription object. I'm saying a client, not a user, because a client is the browser the user is using. A user can have multiple clients—their desktop browsers (each browser is a different client) and their mobile browsers.
3. Store that *PushSubscription* object somewhere on your backend—in the database for example. You need to associate that object with the user of your application—for example store it as a new field in the users table; this depends on your application. To support multiple browsers for the user, you need to store a list of PushSubscription objects instead of one.
4. Get the *PushSubscription* object(s) of the user you want to notify, and then send them the message.
5. Listen to the **push** event in the service worker of your site. Remember service workers run on the frontend side of the site.
6. When the *push* event is triggered, get the message included in the notification sent from the backend, and then display it as a notification using the [Notification API](https://developer.mozilla.org/en-US/docs/Web/API/notification).

The first three steps are only run once for each notification subscription. Once the user is subscribed, it will run the last three steps for each notification you send.

## Display a test notification

To display a notification, you need to get permission from the user first. To do that, you need to run this from your JavaScript:

```js
Notification.requestPermission().then((permissionResult) => {
  if (permissionResult === 'granted') {
    console.log('Can show notifications')
  } 
})
```

Calling this function returns a promise with the permission result. The returned result can be one of these strings: *default*, *granted*, or *denied*. The *default* value is used when the user doesn't click "Allow" or "Block", and instead clicks the close button of the permission prompt.

In some browsers, like Safari, requesting notifications permission requires you to run it from a user gesture, meaning that the user has to do something first to allow for notifications, like clicking a button.

So, to test the above code, put it in a button click handler, like this:

```js
<body>
  <button class="show-notification-button">
    Show Notification
  </button>

  <script>
    const button = document.querySelector('.show-notification-button')

    button.addEventListener('click', () => {
      Notification.requestPermission().then((permissionResult) => {
        if (permissionResult === 'granted') {
          console.log('Can show notifications')
        }
      })
    })
  </script>
</body>
```

After clicking "Show Notification", the browser will ask you if you want to allow notifications from this site or not.

![Step 1](https://tahazsh.com/images/post-images/web-push-notifications-3.png)

If you click "Allow", you will see "Can show notifications" logged into your console.

![Step 2](https://tahazsh.com/images/post-images/web-push-notifications-4.png)

After getting the permission to show notifications, you can start showing notifications using service workers.

Note that installing a service worker on your site is needed for listening to push events and showing notifications.

You can think of a service worker as JavaScript code that runs in the background. This means, the code in service workers can run even if your site isn't loaded or if the browser's window is closed—that's all you need to know about service workers for this tutorial, but you can learn more on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API).

To use a service worker, you have to add it and then register it.

To add it, add a new empty file in your root directory and call it `sw.js` (or any name you want).

And then register it using this:
```js
navigator.serviceWorker.register('/sw.js')
  .then(registration => {})
```

I'm using `.then` here because registering a service worker returns a promise. When that promise is resolved, it gives us the `registration` object, which we will use to subscribe the user and display notifications.

To display a notification, you need to call `registration.showNotification(title, options)`.

So, let's update our code to use the registration object.

```js
const button = document.querySelector('.show-notification-button')

button.addEventListener('click', () => {
  navigator.serviceWorker.register('/sw.js')
    .then((registration) => {
      Notification.requestPermission().then((permissionResult) => {
        if (permissionResult === 'granted') {
          registration.showNotification(
            'My first notification',
            { body: 'More details about this notification' }
          )
        }
      })
    })
})
```

If you click the "Show Notification" button, you will see the notification displayed like this:

![Step 3](https://tahazsh.com/images/post-images/web-push-notifications-5.png)

Here's a quick summary of how that worked:
1. We listened to the button click event.
2. When the button is clicked, we accessed the registration of the service worker.
3. Once the promise is resolved and we have the registration object, we request the notification permission.
4. If the user allowed notifications, we call `showNotification` on the registration object to show the notification with whatever details we want.

## The service worker and the push event

It's not useful to show a notification statically from the client. We should instead show it when we receive a push message. Push messages are sent from a server to the subscribed client.

When the server sends the client a push message, the service worker will trigger an event called `push`. In that event, we can access the message that was sent by the server to the client to display the notification.

Before building that server app, we can test the `push` event from the Chrome's dev tools.

To find that, open up the dev tools, go to the "Application" tab, then click on "Service Workers" from the left sidebar, then you should see some information about your registered service worker. Among other things, you should see an input with a button labelled "Push". If you click the "Push" button, it will trigger the `push` event on this service worker with the text entered in the input.

![Step 4](https://tahazsh.com/images/post-images/web-push-notifications-6.png)

To test that, you need to add the `push` event listener to your service worker file, which we named `sw.js`.

```js
// sw.js
self.addEventListener('push', (event) => {
  const data = event.data
  const message = data.text()
  console.log('message:', message)
})
```

If you click the push button from the dev tools, you will see the message you entered in the input logged into the browser's console.

![Step 5](https://tahazsh.com/images/post-images/web-push-notifications-7.png)

But instead of logging it, you can display it in a notification.

```js
// sw.js

self.addEventListener('push', (event) => {
  const data = event.data
  const message = data.text()
  self.registration.showNotification(message)
})
```

![Step 6](https://tahazsh.com/images/post-images/web-push-notifications-8.png)

So now we understand what the `push` event is for and how to display a notification from it. Next, we will see how to subscribe a user to our push notifications, and then how to send notifications from the server app that we will build.

## How does your server know what client to send the notification to

The server needs to know information about the client it needs to deliver the notification to. To get that information, you need to subscribe the user first. After you subscribe the user, you will get an object called **‌PushSubscription** that contains all the needed information about the client you want to send the notifications to.

The important piece of data in that *PushSubscription* object is the **endpoint** field. This field contains a url that is unique to that client. By client I mean the browser that the user used to subscribe to the notifications—remember, a single user can have multiple clients (browsers).

In the server app, we can use that *endpoint* to send any data we want to the associated client. You will see how in a bit.

## How to make sure it's secure

Imagine that someone has stolen that endpoint. Does this mean any one can send any message to the associated client?

Yes if we don't make it secure using the **‌application server keys**.

The *Application server keys* consist of two keys: public and private keys. We use the private key on the server app side to sign the message we want to send, and then use the public key on the client side to unlock it to access its data.

Note that the *application server keys* are sometimes called the *VAPID* keys (Voluntary Application Server Identity).

Actually, the usage of these keys is required by some browsers, like Chrome. So, we have to use them to make the notifications work in all browsers.

The nice thing about these keys is that generating and using them is a one-time task. And you can get them either using this site: [https://web-push-codelab.glitch.me/](https://web-push-codelab.glitch.me/) or using this [CLI tool](https://github.com/web-push-libs/web-push#command-line).

The important thing is that you should hide the private key from the user. This key is going to be part of your server app, and you can hide it by adding it to your environment variables so you won't push it with your code.

## Subscribe the user to your notifications

To subscribe a user, you need to call this function:

```js
registration.pushManager.subscribe({
  userVisibleOnly: true,
  applicationServerKey: YOUR_PUBLIC_KEY
})
```

This function takes an object with two fields: `userVisibleOnly` and `applicationServerKey`.

The `applicationServerKey` takes the public key you [generated above](https://web-push-codelab.glitch.me/).

The `userVisibleOnly` is a boolean that indicates whether the received message should be shown to the user with a notification or be a silent push. Some browsers like Chrome does not allow silent pushes because the developer might do malicious things like tracking the user's location without the user knowing. So this field should always be *true*.

This function returns a promise; when it's resolved, it gives us the *PushSubscription* object, which we will use as the identifier to the client we want to send push notifications to.

So let's update our code to include the subscription code:

```js
const button = document.querySelector('.show-notification-button')

button.addEventListener('click', () => {
  navigator.serviceWorker.register('/sw.js')
    .then((registration) => {
      Notification.requestPermission().then((permissionResult) => {
        if (permissionResult === 'granted') {
          registration.pushManager.subscribe({
            userVisibleOnly: true,
            applicationServerKey: YOUR_PUBLIC_KEY
          }).then(pushSubscription => {
            console.log('pushSubscription', JSON.stringify(pushSubscription))
          })
        }
      })
    })
})
```

This code is starting to look messy with all these nested `then` callbacks. To clean it up, we can update it to use async/await.

```js
const button = document.querySelector('.show-notification-button')

button.addEventListener('click', async () => {
  const registration = await navigator.serviceWorker.register('/sw.js')

  const permissionResult = await Notification.requestPermission()

  if (permissionResult !== 'granted') {
    return
  }

  const pushSubscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: YOUR_PUBLIC_KEY
  })

  console.log('pushSubscription', JSON.stringify(pushSubscription))
})
```

Next, let's test what we have by clicking "Show Notification". If everything went correctly, then we should see a stringified version of the `pushSubscription` object logged into the browser's console.

This is the value that you need to store in your server's app for that user. As I explained earlier in this article, it's up to you to decide how it should be associated with the user of your app. You might need to store it in the users table in your database. But for simplicity, I'll just copy the logged string from the console and use it directly in the server app that we'll build next.

## Set up a simple Node Express app

You can build the server app using any language and framework you want. For this tutorial, I'm going to stick with JavaScript and build it using Node and Express.

First, create a new folder for the server. For this example I named it *web-push-server*.

Then quickly generate a *package.json* file using:

```bash
npm init -y
```

Then install express:

```bash
npm install express
```

And then create a new JavaScript file in the root directory and name it *index.js*.

Typically, sending a push message would happen after some action on the server. For example, send a notification message to the user when they receive a reply from other users.

For simplicity, I'll just send a notification message when we hit the `/send` endpoint. This endpoint will accept the message title through a query string called *message*.

```js
// index.js

const express = require('express')

const app = express()
const port = 3000

const pushSubscription = JSON.parse(YOUR_PUSH_SUBSCRIPTION_STRING)

app.get('/send', (req, res) => {
  const message = req.query.message || ''
  res.end('Sending message: ' + message)
})

app.listen(port, () => {
  console.log(`web-push-server listening on port ${port}`)
})
```

Before running the app, replace *‌YOUR_PUSH_SUBSCRIPTION_STRING* with the string you logged into the browser's console for the *pushSubscription* object. Remember, it will be different for each client that subscribes to your notifications.

Now if you run the app (with `node index.js`) and go to `/send?message=Hi`, you will see this displayed on the page.

![Step 7](https://tahazsh.com/images/post-images/web-push-notifications-9.png)

If you see this, then our little server is working and we can move on to the next step to send a push message instead of just showing the message on the page.

## Send the notification with the Web Push library

As I mentioned earlier in this article, to send the notification, you need to send a request to the endpoint included in the *PushSubscription* object. Instead of handling this ourselves, we can use the web push library created for that.

Pick the one for the language you are using in your server from this list: https://github.com/web-push-libs/. For this example, we need [the node one](https://github.com/web-push-libs/web-push).

First, let's install it.

```bash
npm install web-push
```

Then, let's import it and use it in our `/send` end point.

```js
// index.js

const express = require('express')
const webpush = require('web-push')

const app = express()
const port = 3000

const pushSubscription = JSON.parse(YOUR_PUSH_SUBSCRIPTION_STRING)

app.get('/send', (req, res) => {
  const message = req.query.message || ''
  webpush.setVapidDetails(
    'mailto:youremail@example.com',
    YOUR_PUBLIC_KEY,
    YOUR_PRIVATE_KEY
  )
  webpush.sendNotification(
    pushSubscription,
    JSON.stringify({ message })
  )
  res.end('Sending message: ' + message)
})

app.listen(port, () => {
  console.log(`web-push-server listening on port ${port}`)
})
```

I did two things here. First, I called `webpush.setVapidDetails` to set the *VAPID* keys. It takes three parameters: your email address, the public key, and the private key—make sure to replace them with your values. I'll explain what this email is for in a little bit.

And then I send the notification using `webpush.sendNotification`. It takes two parameters: the *pushSubscription* object, and the message I want to send. In this example, I'm sending it as a stringified JSON so I can include more data into it.

## Display the notification message on the push event

If you still have the code for testing the *push* event in *sw.js*, then you should see the notification displayed when you hit the `/send` endpoint on your server. However, it will be displayed as a stringified JSON. To fix that, we need to update the code to accept it as a JSON value (using `data.json()`), and then take the *message* value from it.

```js
// sw.js

self.addEventListener('push', (event) => {
  const data = event.data
  const pushMessage = data.json()
  event.waitUntil(
    self.registration.showNotification(pushMessage.message)
  )
})
```

Another new thing you may have noticed here is the use of `event.waitUntil`. This function takes a promise to wait for it to resolve—note that `self.registration.showNotification` returns a promise. We need to wait for that because the browser might terminate the service worker before showing the notification. This can happen because we don't have control over when the browser wakes or terminates the service worker. But with `event.waitUntil` we can force it to wait and keep the service worker running until we display the notification.

To test it, go to the `/send` endpoint with some message in the query string. Once it's loaded, you will see the notification displayed on your device.

![Step 8](https://tahazsh.com/images/post-images/web-push-notifications-10.png)

🎉 Awesome! You now have a working example for adding web push notifications to a site.

There are still two things I haven't talked about in this article. So keep reading!

## The push service

An important thing to understand here is that your server doesn't send the push message directly to the user's browser. Instead it sends it to a web service controlled by your browser's vendor called the *Push Service*.

Each browser vendor uses a different push service. The main responsibility of the push service is to take the push request (which we sent using the web-push library), authenticates it using the public and private keys, and then sends it to the appropriate client.

Note that the push service queues the incoming messages if the user's browser is offline, and then it sends it to the user's browser when it's online.

We as developers don't have control over how the push service works or which one to use in the browser. However, all browsers have a standardized request spec called the *web push protocol request*. The push messages will work as long as they follow that request spec—using the web-push library saves us from all that work.

In the above code, we had to provide an email when sending the push message. That email is provided to the *push service* in case it needs to contact the sender.

## Web push on mobile devices

The code we wrote in this article should work on mobile devices as well. However, for apple devices with iOS/iPadOS, you should add the site to the home screen and make sure you have `display` set to `standalone` or `fullscreen` in your *manifest.json*. For more information, read [the apple's doc](https://webkit.org/blog/13878/web-push-for-web-apps-on-ios-and-ipados/).

## For more details

This article covers the basic workflow of adding web push notifications to your site. This is almost all you need to know to use it for any web app you have; however, there are things that I haven't talked about in this article, like customizing the behavior and the UI of the displayed notification. If you need to get into more details, check out this list of in-depth articles on [web.dev](https://web.dev/notifications/).

---

Thanks for Reading ❤️! Follow me on [Twitter](https://twitter.com/tahazsh) for updates about my latest blog posts, video tutorials, and some cool web dev tips. Let's be friends!