Push notifications are a powerful way to engage users and bring them back to your application. In this tutorial, we'll implement push notifications in a Next.js application using Firebase Cloud Messaging (FCM). We'll cover setting up Firebase, configuring permissions, and handling notifications in both foreground and background states.

Note: This guide is based on Next.js 15 or later. Ensure your project uses this version to follow along.

Prerequisites

  • Basic knowledge of TypeScript and Next.js
  • A Firebase account
  • Node.js installed on your machine

Step 1: Configure Firebase in Your Project

First, create a new Firebase project:

  1. Go to the Firebase Console
  2. Create a new project (or use an existing one)
  3. Add a web app to your project
  4. Install the Firebase SDK in your Next.js project:
npm install firebase

Create a firebase-config.ts file in your project to configure Firebase:

// firebase-config.ts
import { initializeApp } from "firebase/app";
import { getMessaging, getToken, onMessage } from "firebase/messaging";

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
};

let messaging: ReturnType<typeof getMessaging>;

if (typeof window !== "undefined" && "navigator" in window) {
  const app = initializeApp(firebaseConfig);
  messaging = getMessaging(app);
}

export { messaging, getToken, onMessage };

Step 2: Setting Up Service Worker

Firebase Cloud Messaging requires a service worker to handle background notifications. Create a file called firebase-messaging-sw.js in your public directory:

// firebase-messaging-sw.js
importScripts(
  "https://www.gstatic.com/firebasejs/10.11.1/firebase-app-compat.js"
);
importScripts(
  "https://www.gstatic.com/firebasejs/10.11.1/firebase-messaging-compat.js"
);

const firebaseConfig = {
  apiKey: "YOUR_API_KEY",
  authDomain: "YOUR_AUTH_DOMAIN",
  projectId: "YOUR_PROJECT_ID",
  storageBucket: "YOUR_STORAGE_BUCKET",
  appId: "YOUR_MESSAGING_SENDER_ID",
  messagingSenderId: "YOUR_APP_ID",
};

firebase.initializeApp(firebaseConfig);

const messaging = firebase.messaging();

messaging.onBackgroundMessage((payload) => {
  const notificationTitle = payload.notification.title;
  const notificationOptions = {
    body:
      payload.notification.body,
      icon: payload.notification.icon,
    data: { url: payload.fcmOptions?.link || "/" },
  };

  self.registration.showNotification(notificationTitle, notificationOptions);
});

self.addEventListener("notificationclick", (event) => {
  event.notification.close();
  const targetUrl = event.notification.data?.url || "/";

  event.waitUntil(
    clients
      .matchAll({
        type: "window",
        includeUncontrolled: true,
      })
      .then((clientList) => {
        for (const client of clientList) {
          if (client.url.includes(targetUrl) && "focus" in client) {
            return client.focus();
          }
        }
        return clients.openWindow(targetUrl);
      })
  );
});

Important Note: You should not use environment variables in the service worker file as it's served directly by the browser. For production, you would need to implement a build step that replaces these values.

Step 3: Request Notification Permission

1. Requesting Permission

Request user permission for push notifications, as explicit consent is required. If needed, save the retrieved token to your database within this function.

// requestNotificationPermission.ts
import { getToken, messaging } from "./firebase";

export const requestNotificationPermission = async () => {
  try {
    const permission = await Notification.requestPermission();

    if (permission !== "granted") {
      return;
    }

    const registration = await navigator.serviceWorker.register(
      "/firebase-messaging-sw.js"
    );

    if (!registration) {
      return;
    }

    const token = await getToken(messaging, {
      vapidKey: process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY,
      serviceWorkerRegistration: registration,
    });

    if (!token) {
      return;
    }

    // Save the Token to a Database here if you need

  } catch (error) {
    console.error("Error Requesting Notification Permission:", error);
    return;
  }
};

2. Creating a Notification Component

To handle incoming notifications elegantly, create a dedicated Notification Permission component:

// components/NotificationPermission.tsx
"use client";

import { useEffect } from "react";
import { requestNotificationPermission } from "@/lib/firebase/requestNotificationPermission";

const NotificationPermission = () => {

  useEffect(() => {
    const subscribeUser = async () => {
        await requestNotificationPermission(user.id);
      };

    subscribeUser();
  }, []);

  return null;
};

export default NotificationPermission;

3. Adding the Notification Component

Integrate the Notification component into your application layout to ensure it is always active:

// layout.tsx
import NotificationPermission from "@/components/notification/NotificationPermission";
...

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>
        <NotificationPermission/>
        {children}
      </body>
    </html>
  );
}

Step 4: Adding the firebase-admin.json

In order to send notifications from your server, you will need to integrate Firebase Admin SDK. This requires adding the firebase-admin.json configuration file to your project.

  1. Go to the Firebase Console.
  2. Under Project Settings > Service Accounts, generate a new private key.
  3. Download the key file, which is named firebase-admin.json.
  4. Place the firebase-admin.json file in your project’s root or a secure folder.

Now, you’ll need to install the firebase-admin package:

npm install firebase-admin

Finally, configure Firebase Admin SDK to use this file by adding the following code to your backend:

// firebaseAdmin.ts
import admin from "firebase-admin";
import settings from "../../../config/firebase-admin.json";

const serviceAccount = settings as admin.ServiceAccount;

if (!admin.apps.length) {
  try {
    admin.initializeApp({
      credential: admin.credential.cert(serviceAccount),
    });
    console.log("Firebase Admin Initialized Successfully");
  } catch (error) {
    console.error("Firebase Admin Initialization Failed:", error);
  }
}

export default admin;

Step 5: Sending Notifications via Firebase Admin SDK

Add the following function to handle sending notifications:

// sendNotification.ts
import admin from "@/lib/firebase/firebaseAdmin";

interface Recipient {
  endpoint: string;
}

export const sendNotification = async (
  title: string,
  body: string,
  recipients: Recipient[]
) => {
  try {
    const response = await admin.messaging().sendEachForMulticast({
      tokens: recipients.map((recipient) => recipient.endpoint),
      notification: { title, body },
    });

    console.log(
      `Notification sent! Success: ${response.successCount}, Failures: ${response.failureCount}`
    );

    if (response.failureCount > 0) {
      response.responses.forEach((res, index) => {
        if (
          !res.success &&
          res.error?.code === "messaging/registration-token-not-registered"
        ) {
          console.warn(`Invalid token found: ${recipients[index].endpoint}`);
        }
      });
    }
  } catch (error) {
    console.error("Error sending notifications:", error);
  }
};

Step 6: Creating the Notification API

To send notifications programmatically, you need an API endpoint. Create an API route in your Next.js application:

// pages/api/notifications/route.ts
import { NextRequest, NextResponse } from "next/server";
import { sendNotification } from "@/lib/firebase/sendNotification";

export async function POST(req: NextRequest) {
  try {
    const body = await req.json();

    const { title, body, recipients } = body;

    if (!title || !body || !recipients) {
        return NextResponse.json(
          { success: false, message: "Missing required fields" },
          { status: 400 }
        );
    }

    await sendNotification(title, body, recipients);
    return res.status(200).json({ message: "Notification sent successfully" });

  } catch (error) {
    console.error("Error sending notification:", error);
    res.status(500).json({ error: "Failed to send notification" });
  }
}

To call this API, send a POST request to /api/notifications with a JSON body containing the title, body, and recipients. The title and body should be strings, and recipients should be an array of FCM tokens.

Step 7: Environment Setup

Ensure that you add the required environment variables:

// .env.local
NEXT_PUBLIC_FIREBASE_API_KEY=your-api-key
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
NEXT_PUBLIC_FIREBASE_PROJECT_ID=your-project-id
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=your-project.appspot.com
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=your-sender-id
NEXT_PUBLIC_FIREBASE_APP_ID=your-app-id
NEXT_PUBLIC_FIREBASE_VAPID_KEY=your-vapid-public-key
  • Get the first 6 variables from your Firebase Console: All of these can be found under Project Settings > General.
  • Get a VAPID key from the Web configuration section in Firebase Console > Project Settings > Cloud Messaging under "Web Push certificates."

⚠️ Security Considerations

When working with push notifications, always keep the following in mind:

  1. Never expose your Firebase Admin SDK credentials in client-side code
  2. Store environment variables securely
  3. Implement proper validation and authentication for your notification API
  4. Consider rate limiting to prevent abuse

Conclusion

By setting up Firebase Cloud Messaging (FCM) with Web Push notifications in Next.js, you’ve enabled an effective way to engage your users in real-time. However, always stay mindful of security best practices, keep an eye on evolving web push standards, and regularly check Firebase documentation for new updates and features.