Push notifications with Firebase
Tags: firebase, react, nextjs, node
Published: 2022-05-29 09:55:55

---
title: Push notifications with Firebase
published: true
tags: ['firebase', 'react', 'nextjs', 'node']
canonical_url: https://sevic.dev/notes/push-notifications-firebase/
---

Push notifications are a great alternative to email notifications. There is no need for a verification step, UX is improved, and user engagement with the app is increased. This post covers both front-end and back-end setups.

### Requirements for the push notifications

- `firebase` package installed
- Created Firebase [project](https://console.firebase.google.com/u/0/)
- Firebase configuration can be found on Project Overview &rarr; Project settings &rarr; General &rarr; Your apps
- Public Vapid key can be found on Project Overview &rarr; Project settings &rarr; Cloud Messaging &rarr; Web Push certificates (used on the front-end)
- Firebase messaging service worker
- Project ID can be found on Project Overview &rarr; Project settings &rarr; General tab
- Server key for sending the push notifications (used on the back end)
- HTTPS connection (localhost for local development)

### Front-end setup

#### Firebase messaging service worker

The following service worker should be registered for handling background notifications. A custom `notificationclick` handler should be implemented before importing firebase libraries. The below implementation opens a new window with the defined URL if it is not already open. Firebase automatically checks for service workers at `/firebase-messaging-sw.js`, so it should be publicly available.

```js
// /firebase-messaging-sw.js
/* eslint-disable no-unused-vars */
self.addEventListener('notificationclick', (event) => {
  event.notification.close();
  const DEFAULT_URL = '<URL>';
  const url =
    event.notification?.data?.FCM_MSG?.notification?.click_action ||
    DEFAULT_URL;
  event.waitUntil(
    clients.matchAll({ type: 'window' }).then((clientsArray) => {
      const hadWindowToFocus = clientsArray.some((windowClient) =>
        windowClient.url === url ? (windowClient.focus(), true) : false
      );
      if (!hadWindowToFocus)
        clients
          .openWindow(url)
          .then((windowClient) => (windowClient ? windowClient.focus() : null));
    })
  );
});

importScripts('https://www.gstatic.com/firebasejs/9.19.1/firebase-app-compat.js');
importScripts('https://www.gstatic.com/firebasejs/9.19.1/firebase-messaging-compat.js');

const firebaseApp = initializeApp({
  apiKey: 'xxxxxx',
  authDomain: 'xxxxxx',
  projectId: 'xxxxxx',
  storageBucket: 'xxxxxx',
  messagingSenderId: 'xxxxxx',
  appId: 'xxxxxx',
  measurementId: 'xxxxxx'
});

const messaging = getMessaging(firebaseApp);
```

#### Helper functions

##### `getToken`

- generates a unique registration token for the browser or gets an already generated token
- requests permission to receive push notifications
- triggers the Firebase messaging service worker

There are multiple types of errors as response:

- code `messaging/permission-blocked` - user blocks the notifications
- code `messaging/unsupported-browser` - user's browser doesn't support the APIs required to use the Firebase SDK
- code `messaging/failed-service-worker-registration` - there's an issue with the Firebase messaging service worker

The access token is invalidated when a user manually blocks the notifications in the browser settings.

##### `isSupported`

- checks if all required APIs for push notifications are supported
- returns `Promise<boolean>`

It should be used in `useEffect` hooks.

```js
import { isSupported } from 'firebase/messaging';

// ...
useEffect(() => {
  isSupported()
    .then((isAvailable) => {
      if (isAvailable) {
        // ...
      }
    })
    .catch(console.error);
}, []);
// ...
```

##### `initializeApp`

- should be called before the app starts

```js
import { initializeApp } from 'firebase/app';
import { getMessaging, getToken } from 'firebase/messaging';
import { firebaseConfig } from 'constants/config';

export const initializeFirebase = () => initializeApp(firebaseConfig);

export const getTokenForPushNotifications = async () => {
  const messaging = getMessaging();
  const token = await getToken(messaging, {
    vapidKey: process.env.NEXT_PUBLIC_VAPID_KEY
  });
  return token;
};
```

### Back-end setup

#### Server keys

The server key for API v1 can be derived from the service account key JSON file. In that case, the JSON file should be encoded and stored in the environment variable to prevent exposing credentials in the repository codebase. The service account key JSON file can be downloaded by clicking Generate new private key on the Project Overview &rarr; Project settings &rarr; Service accounts tab.

```ts
import * as serviceAccountKey from './service-account-key.json';

const encodedServiceAccountKey = Buffer.from(
  JSON.stringify(serviceAccountKey)
).toString('base64');

process.env.SERVICE_ACCOUNT_KEY = encodedServiceAccountKey;
```

```ts
import 'dotenv/config';
import * as googleAuth from 'google-auth-library';

(async () => {
  const serviceAccountKeyEncoded = process.env.SERVICE_ACCOUNT_KEY;
  const serviceAccountKeyDecoded = JSON.parse(
    Buffer.from(serviceAccountKeyEncoded, 'base64').toString('ascii')
  );
  const jwt = new googleAuth.JWT(
    serviceAccountKeyDecoded.client_email,
    null,
    serviceAccountKeyDecoded.private_key,
    ['https://www.googleapis.com/auth/firebase.messaging'],
    null
  );
  const tokens = await jwt.authorize();
  const authorizationHeader = `Bearer ${tokens.access_token}`;
  console.log(authorizationHeader);
})();
```

#### Manually sending the push notification

The icon URL should be covered with HTTPS, so the notification correctly shows it.

- API v1

```ts
// ...
try {
  const response = await axios.post(
    `https://fcm.googleapis.com/v1/projects/${projectId}/messages:send`,
    {
      message: {
        notification: {
          title: 'Push notifications with Firebase',
          body: 'Push notifications with Firebase body'
        },
        webpush: {
          fcmOptions: {
            link: 'http://localhost:3000'
          },
          notification: {
            icon: 'https://picsum.photos/200'
          }
        },
        token: registrationToken
      }
    },
    {
      headers: {
        Authorization: authorizationHeader
      }
    }
  );
  console.log(response.data);
} catch (error) {
  console.error(error?.response?.data?.error);
}
// ...
```

A successful response returns an object with `name` key, which presents the notification id in the format `projects/{project_id}/messages/{message_id}`.

There are multiple types of errors in the response:

- code 400 - request body is not valid
- code 401 - the derived token is expired
- code 404 - registration token was not found

### Demo

The demo with the mentioned examples is available [here](https://sevic.dev/demos).