My FeedDiscussionsHeadless CMS
New
Sign in
Log inSign up
Learn more about Hashnode Headless CMSHashnode Headless CMS
Collaborate seamlessly with Hashnode Headless CMS for Enterprise.
Upgrade ✨Learn more

Implementing push notifications with Firebase for Javascript apps

Sergiy C.'s photo
Sergiy C.
·Jan 23, 2022·

8 min read

These notifications we're used to on smartphones are available in browser environments too. If you know why are you here exactly, skip this part and jump directly to the solution.

And these notifications on the web that we hate because every spam site asks our permissions about sending us their spam. However, the technology itself is useful. I didn't implement web push notifications before. There was no such case, but recently I needed to notify my users about important information as soon as possible. Email is good, but a user should open an email client first. With notifications, you see the popup immediately.

I decided to write this article because I didn't find comprehensive information about implementing browser notifications for React, Next.JS, Node apps. We'll be using Firebase for this purpose, to save time ourselves by not building solutions from scratch.

Overview of browser push notifications

So as we said, it's like those mobile notifications but used in browsers mostly for spamming. It's worth noting, that you need to send them to a user if that's really what he subscribed to. Examples:

  • new comments under the user's post;
  • new message on some platform;
  • important information that should be handled fast;

The other non-important things may go to email.

How does it work

First, you ask a user permission to show the notifications. If you get an acceptance, your website installs a service worker that'll be handling the notifications. You also send the request to register this particular user in a messaging server, you'll be asking it to send a notification to someone.

Push notifications overview

When a messaging server registers your users, it'll send you a unique for your user token that you'll be using as an addressee to send push notifications programmatically.

Firebase push notifications

You save the registration token from a messaging server. When you want to send a notification, you'll point out this token for the user you want to send a message to, thus the messaging server understands to whom to send the notification. When a user agrees to receive notifications, your website installs a service worker, it's a background script that'll be running on the user's browser. It's programmed to handle the messages from the messaging server. When it receives one, it'll assemble a notification to show to this user.

Messaging server? This is any server that knows how to communicate with your service worker. You can build it by yourself and code a service worker that will be managing messages from there. But we won't complicate our life and we'll be using Firebase.

Firebase push notifications

If we use Firebase, we don't care about the proper messaging server setup because we got covered. What we need is to code logic to ask for the notifications permissions, install a service worker and write a logic to send notifications from our app.

For further setup, you should create a project in the Firebase Console, and have config from there(a JSON file).

Front-end set up

I have a Next.js app, but this algorithm covers any app written in Javascript, it's a library- and framework-independent.

Install Firebase to your project so we can leverage simple functions rather than doing requests to FCM(Firebase Cloud Messaging) manually.

$ npm install firebase
# or
$ yarn add firebase

Find a place where do you want to ask a user about the notification permission. For example, it can be a button that says "Subscribe to browser notifications". On this button click, you'll be calling a function getFCMToken() written below:

import { initializeApp } from 'firebase/app';
import { getMessaging, getToken } from 'firebase/messaging';

// Replace these values with your project's ones
// (you can find such code in the Console)
const firebaseConfig = {
    apiKey: 'xxxxx-xxx',
    authDomain: 'xxxx.firebaseapp.com',
    projectId: 'xxxx-xxxx',
    storageBucket: 'xxxx.appspot.com',
    messagingSenderId: '00000000',
    appId: '0:00000:00000000'
};

export const app = initializeApp(firebaseConfig);
const messaging = getMessaging();

export async function getFCMToken() {
    try {
        // Don't forget to paste your VAPID key here
        // (you can find it in the Console too)
        const token = await getToken(messaging, { vapidKey: <YOUR_VAPID_KEY> });
        return token;
    } catch (e) {
        console.log('getFCMToken error', e);
        return undefined
    }
}

With this code, we initialize the Firebase library and write this getFCMToken() function. It retrieves a registration token from FCM, and it also asks a user for notification permission. If the permissions are accepted, only then it'll communicate with FCM to register this user. Otherwise, the code throws an error, which you catch in the catch block.

Then, you get an FCM token(a user's unique token in the FCM system), which you'll be using to send notifications. So you need to store it somewhere. Usually, you have a server where you may send the token and it'll save it in the database for this particular user. Otherwise, you won't be able to send notifications to users. It's required to have the Firebase Admin SDK, which is available on server environments.

There are some exceptions though. In some cases when you want only to subscribe users to your notifications like in a newsletter, you may not store the FCM tokens. Firebase has them and you can send the notifications manually from the Console. But it's not possible to send them automatically(programmatically) because you can't differentiate users(you don't have the tokens).

And the last thing is to have a service worker that will handle the notifications from FCM. Create a file that'll be available on the root of your web app, the file named firebase-messaging-sw.js. It should be accessible on https://yourwebsite.com/firebase-messaging-sw.js. Its contents:

// It's a static script file, so it won't be covered by a module bundling system
// hence, it uses "importScripts" function to load the other libs
importScripts('https://www.gstatic.com/firebasejs/8.2.0/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/8.2.0/firebase-messaging.js');

// Replace the values with yours
const firebaseConfig = {
    apiKey: 'xxx',
    authDomain: 'xxx',
    projectId: 'xxx',
    storageBucket: 'xxx',
    messagingSenderId: 'xxx',
    appId: 'xxx'
};

firebase.initializeApp(firebaseConfig);

const messaging = firebase.messaging();

// Not necessary, but if you want to handle clicks on notifications
self.addEventListener('notificationclick', (event) => {
    event.notification.close()

    const pathname = event.notification?.data?.FCM_MSG?.notification?.data?.link
    if (!pathname) return
    const url = new URL(pathname, self.location.origin).href

    event.waitUntil(
        self.clients
            .matchAll({ type: 'window', includeUncontrolled: true })
            .then((clientsArr) => {
                const hadWindowToFocus = clientsArr.some((windowClient) =>
                    windowClient.url === url ? (windowClient.focus(), true) : false
                )

                if (!hadWindowToFocus)
                    self.clients
                        .openWindow(url)
                        .then((windowClient) =>
                            windowClient ? windowClient.focus() : null
                        )
            })
    )
})

That's all on the front-end side! You can test your button, on the press, it should ask your permission(a browser asks, to be precise) to send you notifications. When you allow it, you should see an FCM token(console.log it somewhere)

Sending notifications from the server

In my case, it's a Node.js server and we'll be installing the SDK for it, but the general principle is the same for other languages/platforms.

$ npm install firebase-admin

You also have a configuration for the backend in the Console. It differs from the client-side one because it has a private key that you need to sign your notification, which will be sent to FCM. Put this firebase.json file(it's called service account) somewhere to be accessible from code, you may put it as an environment variable.

Then you should initialize the Firebase library on a server start(or later if you want to control the flow). I've put this logic into a separate file:

import admin from 'firebase-admin';

import serviceAccount from './config/firebase.json';

export function init() {
    admin.initializeApp({
        credential: admin.credential.cert(serviceAccount),
    });
}

On a server start, call init() and Firebase is ready to serve you.

I won't cover storing the FCM tokens, I'm sure you know how to do it and it's not the purpose of this article. So, given you have an initialized Firebase on the server(from the last paragraph) and you have a user's FCM token, you're ready to send push notifications to a user's browser! It looks like this:

import { getMessaging } from 'firebase-admin/messaging';

// I use Typescript, you may not, but types will help you
// to understand what data structures FCM expects.
// It's an internal structure though, firebase-admin has
// good typings in the library
interface Message {
    title: string;
    body: string;
    requireInteraction?: boolean;
    link?: string;
}

// Use this function to send push notifications to a specific user
export async function sendFCMMessage(fcmToken: string, msg: Message): Promise<string> {
    try {
        const res = await getMessaging().send({
            webpush: {
                notification: {
                    ...msg,
                    icon: 'https://your-website.com/favicon.png',
                    requireInteraction: msg.requireInteraction ?? false,
                    actions: [{
                        title: 'Open',
                        action: 'open',
                    }],
                    data: {
                        link: msg.link,
                    },
                },
            },
            token: fcmToken,
        });
        return res;
    } catch (e) {
        console.error('sendFCMMessage error', e);
    }
}

Now, some details on the notification payload. Firebase supports various platforms, here I use the webpush field for my payload. FCM supports other fields:

interface BaseMessage {
    data?: {
        [key: string]: string;
    };
    notification?: Notification;
    android?: AndroidConfig;
    webpush?: WebpushConfig;
    apns?: ApnsConfig;
    fcmOptions?: FcmOptions;
}

I've tried to use notification as a general-purpose one, but I had issues with clicking on notifications, a browser didn't handle clicks(the service worker had the click handler). Plus, there were problems with showing icons on notifications. It's better to use webpush if you target desktop users. An FCM token can be for various platforms: Android, iOS, web.

Inside webpush, there are title and body that correspond to a notification's title and body. There's icon if you want your notification to have an icon. Put a publicly accessible image you want to be shown. Set requireInteraction to true if you don't want the notification to be closed after a few seconds, it should wait for the user's explicit reaction.
There's a custom link field inside data, it's aimed for the service worker to be read and handle the click on notifications correctly.

I don't know about all browsers, but my browser(Brave) doesn't handle the default notification click on the whole area, there should be custom buttons. I define them in the actions field. It seems it doesn't matter what buttons with actions I put, the "Open" button(action) will open the URL from data.link property I send.

Summary

Web push notifications aren't difficult to implement if you can use Firebase. Also, it's easy to send messages to various devices on different platforms. Only obtain an FCM token client-side(on a mobile, web, or desktop), and send notifications from a server.

Source