Push notifications with Firebase

May 29, 2022

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.

Requirements for the push notifications

  • Created Firebase project
  • Project ID, can be found on Project settings General tab
  • Server key for sending the push notifications (used on the back-end)
  • Public Vapid key, can be found on Project settings Cloud Messaging Web Push certificates (used on the front-end)
  • Firebase configuration, can be found on Project settings General Your apps
  • Firebase messaging service worker
  • HTTPS connection (localhost for local development)
  • firebase package installed

Helper functions


  • generates unique token for the browser or gets already generated token
  • requests permission for receiving push notifications
  • triggers the Firebase messaging service worker

If the user blocks the push notifications, FirebaseError error with code messaging/permission-blocked is thrown. If the user's browser doesn't support the APIs required to use the Firebase SDK, FirebaseError error with code messaging/unsupported-browser is thrown. The access token is invalidated when a user manually blocks the notifications in the browser settings.


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

It should be used in useEffect hooks.

import { isSupported } from 'firebase/messaging';
// ...
useEffect(() => {
.then((isAvailable) => {
if (isAvailable) {
// ...
}, []);
// ...


  • should be called before the app starts
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;

Firebase messaging service worker

The following service worker should be registered for handling background notifications. 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.

// /firebase-messaging-sw.js
/* eslint-disable no-unused-vars */
self.addEventListener("notificationclick", (event) => {
const DEFAULT_URL = "<URL>";
const url =
event.notification?.data?.FCM_MSG?.notification?.click_action ||
clients.matchAll({ type: "window" }).then((clientsArray) => {
const hadWindowToFocus = clientsArray.some((windowClient) =>
windowClient.url === url ? (windowClient.focus(), true) : false
if (!hadWindowToFocus)
.then((windowClient) => (windowClient ? windowClient.focus() : null));
let messaging = null;
try {
if (typeof importScripts === "function") {
apiKey: "xxxxxx",
authDomain: "xxxxxx",
projectId: "xxxxxx",
storageBucket: "xxxxxx",
messagingSenderId: "xxxxxx",
appId: "xxxxxx",
measurementId: "xxxxxx",
messaging = firebase.messaging();
} catch (error) {

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 Project settings Service accounts tab. The server key for the legacy API can be found on Project settings Cloud Messaging Cloud Messaging API (Legacy), if it is enabled.

import * as serviceAccountKey from './serviceAccountKey.json';
const encodedServiceAccountKey = Buffer.from(
process.env.SERVICE_ACCOUNT_KEY = encodedServiceAccountKey;
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(
const tokens = await jwt.authorize();
const authorizationHeader = `Bearer ${tokens.access_token}`;

Manually sending the push notification

Icon URL should be covered with HTTPS so the icon can be properly shown in the notification.

  • legacy
curl --location --request POST 'https://fcm.googleapis.com/fcm/send' \
--header 'Authorization: key=<SERVER_KEY>' \
--header 'Content-Type: application/json' \
--data-raw '{
"notification": {
"title": "Push notifications with Firebase",
"body": "Push notifications with Firebase body",
"click_action": "http://localhost:3000",
"icon": "https://picsum.photos/200"
"to": "<TOKEN>"

The response contains success key with 1 value when the push notification is successfully sent. The response contains failure key with 1 value when sending the push notification failed, in this case, results key is an array with error objects, some of the error names are InvalidRegistration and NotRegistered.

  • API v1
curl --location --request POST 'https://fcm.googleapis.com/v1/projects/<PROJECT_ID>/messages:send' \
--header 'Authorization: Bearer <TOKEN_DERIVED_FROM_SERVICE_ACCOUNT_KEY>' \
--header 'Content-Type: application/json' \
--data-raw '{
"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": "<TOKEN>"

Successful response return JSON with name key which presents the notification id in the format projects/{project_id}/messages/{message_id}. Error with code 400 is thrown when request body is not valid. Error with code 401 is thrown when the derived token is expired.

Progressive Web Apps 101

March 5, 2022

Progressive Web Apps bring some advantages over native mobile apps

  • automatic updates can be implemented
  • the installed app takes less memory
  • installable on phones, tablets, desktops

Prerequisites for installation

  • web app is running over an HTTPS connection
  • service worker is registered
  • web app manifest (manifest.json) is included

Service worker

Read more about it on Caching with service worker and Workbox


Following fields can be included

  • name is a full name used when the app is installed
  • short_name is a shorter version of the name that is shown when there is insufficient space to display the full name
  • background_color is used on a splash screen
  • description is shown on an installation pop-up
  • display customizes which browser UI is shown when the app is launched (standalone, fullscreen, minimal-ui, browser)
  • icons is a list of icons for the browser used in different places (home screen, app launcher, etc.)
  • scope specifies the navigation scope of the PWA. It should start with the URL from start_url value. If the user navigates outside the scope, PWA won't be open.
  • screenshots is a list of screenshots shown on the installation pop-up
  • start_url is a relative URL of the app which is loaded when the installed app is launched. PWA usage can be tracked by adding UTM parameters within the URL.
  • theme_color sets the color of the toolbar, it should match the meta theme color specified in the document head

Description and screenshots are shown only on mobile phones.

"name": "App name",
"short_name": "App short name",
"background_color": "#ffffff",
"description": "App description",
"display": "standalone",
"icons": [
"src": "icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png"
"src": "icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png"
"src": "icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png"
"src": "icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
"src": "icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
"scope": "/app",
"screenshots": [{
"src": "screenshots/main.jpg",
"sizes": "1080x2400",
"type": "image/jpg"
"start_url": "/app?utm_source=pwa&utm_medium=pwa&utm_campaign=pwa",
"theme_color": "#3366cc"

Manifest file should be included via link tag

<link rel="manifest" href="/manifest.json">

In-app installation experience

It can be implemented on Google Chrome and Edge.

  • listen for the beforeinstallprompt event
  • save beforeinstallprompt event so it can be used to trigger the installation
  • provide a button to start the in-app installation flow
let deferredPrompt;
let installable = false;
window.addEventListener("beforeinstallprompt", (event) => {
deferredPrompt = event;
installable = true;
document.getElementById("installable-btn").innerHTML = "Install";
window.addEventListener("appinstalled", () => {
installable = false;
document.getElementById("installable-btn").addEventListener("click", () => {
if (installable) {
deferredPrompt.userChoice.then((choiceResult) => {
if (choiceResult.outcome === "accepted") {
document.getElementById("installable-btn").innerHTML = "click!";
} else {


chrome://webapks page on mobile phones shows the list of installed PWAs with their details. Last Update Check Time is useful for checking when the manifest file was updated. The app is updated once a day if there are some manifest changes.


A working example is available at https://github.com/zsevic/pwa-starter


© 2022