Keeping users informed and engaged is crucial (of course, you know it π) for any great application, product or system. Thatβs where push notifications come inβdelivering instant updates directly to users, even when the app is closed.
We are going to build our own(push notification) with nextjs and FCM. The brilliant point is that FCM is completely free to use, even for commercial applications... and yeah, I love free stuffπ.
Overview: What Weβll Do π¨πΌβπ»
- Set up an FCM project: - Comprehensive guide on how to set up a Firebase Cloud Messaging project.
- Ask for notification permission: - Prompt the user for permission; this step is crucial since notifications require explicit user consent.
- Register a service worker: - If permission is granted, we register the service worker. This ensures that background push events can be handled even if the app is closed (not actively in use).
- Retrieve the FCM token: β Once the service worker is registered, we request an FCM token.This token acts as the unique address for Firebase to deliver push notifications to this specific device(where the service worker is registered), even when the app is closed.
- Store the token: - Store the token to your database sync with the client-side and be able to send notifications to the appropriate client. In this case we are going to use supabase, but feel free to use your favorite database.
- Trigger notification: - Use a backend service to send push messages, which then delivers notifications to the subscribed users through the service worker.
With these steps, our push notification system will be ready to roll.
Enough talk, let's build π
Setting up a FCM project
- Head over to your firebase console and "Create a firebase project", continue with the other steps and ensure to disable google analytics(unless you really need it). Then hit "Create project" its all pretty straight foward.
- Enable Cloud messaging and choose web as you platform, add a nickname for your project, add firebase SDK to your project using
npm install firebase
. - Copy the configuration code and paste it in your firebaseConfig file and ensure that it only runs on the client-side, like this:
/lib/firebase/firebaseConfig.js
// Initialize Firebase for the frontend (Client-Side)
import { getMessaging, getToken, onMessage } from "@firebase/messaging";
import { initializeApp } from "firebase/app";
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "YOUR_AUTH_DOMAIN",
projectId: "YOUR_PROJECT_ID",
storageBucket: "YOUR_STORAGE_BUCKET",
messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
appId: "YOUR_APP_ID"
};
// Ensure to initialize Firebase only when in the client-side
let messaging;
if (typeof window !== "undefined" && "navigator" in window) {
const app = initializeApp(firebaseConfig);
messaging = getMessaging(app);
}
export {messaging, getToken, onMessage }
Environment variables
Create a .env.local file at the root directory and add the environment variables. We will walk step-by-step on where to get them. Get the first 6 variables from the firebaseConfig file.
Next, go to your project settings in firebase and navigate to service account, scroll down to "Generate new private key" then "Generate key". This will download a json file with authentication credentials that allow secure interactions with firebase messaging services.
Don't save the json file anywhere in your project, I would recommend you to copy the values that are relevant for our project i.e., "private_key_id", "private_key" and "client_email" then safely delete it.
The last variables are the VAPID keys (public and private). Back to your project settings(hope this is the last time π€), navigate to cloud messaging and scroll down to web configuration, copy the "key pair"(this is the public key), click the more horizontal icon to generate the private key and copy it too.
Double check that every variable is present(should be 12 in number) and they are consistent to nextjs conventions. Your final .env.local should look like this:
# Firebase SDK credentails for front-end interactions
NEXT_PUBLIC_FIREBASE_API_KEY="YOUR_API_KEY"
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN="YOUR_AUTH_DOMAIN"
NEXT_PUBLIC_FIREBASE_PROJECT_ID="YOUR_PROJECT_ID"
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET="YOUR_STORAGE_BUCKET"
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID="YOUR_MESSAGING_SENDER_ID"
NEXT_PUBLIC_FIREBASE_APP_ID="YOUR_APP_ID"
NEXT_PUBLIC_FIREBASE_PRIVATE_KEY_ID="YOUR_VAPID_PUBLIC_KEY"
NEXT_PUBLIC_FIREBASE_VAPID_PUBLIC_KEY="YOUR_VAPID_PUBLIC_KEY"
# Firebase Admin SDK Private Key (for server authentication)
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nYOUR_PRIVATE_KEY_HERE\n-----END PRIVATE KEY-----\n"
# Firebase SDK credentials for server-side interactions
FIREBASE_VAPID_PRIVATE_KEY="YOUR_VAPID_PRIVATE_KEY"
FIREBASE_PROJECT_ID="YOUR_PROJECT_ID"
FIREBASE_CLIENT_EMAIL="YOUR_CLIENT_EMAIL"
Ensure to replace every placeholder with your actual credentials.
Having that in place, you can now close your firebase tab, close your .env.local file, and lets continue πΆπΌββοΈββ‘οΈ.
Service worker file
- On the public directory, create a service worker file.
- β οΈ Important: Service workers run on a separate thread and cannot directly access environment variables. Ensure you hardcode your actual credentials in this file instead of relying on environment variables.
/firebase-messaging-sw.js
// Purpose: Handle incoming push notifications with firebase
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");
// Initialize Firebase with your project credentials
firebase.initializeApp({
apiKey: "YOUR_API_KEY",
authDomain: "YOUR_AUTH_DOMAIN",
projectId: "YOUR_PROJECT_ID",
storageBucket: "YOUR_STORAGE_BUCKET",
appId: "YOUR_APP_ID",
messagingSenderId: "YOUR_MESSAGING_SENDER_ID"
})
// Initialize FCM to enable this(sw) to receive push notifications from firebase servers
const messaging = firebase.messaging()
// Listen to background messages from firebase servers
messaging.onBackgroundMessage((payload) => {
// Extract the required details from the payload
const notificationTitle = payload.notification?.title || "Notification";
const notificationOptions = {
body: payload.notification?.body || "You have a new message. Please check it out",
icon: "/assets/icon.webp" || payload.notification?.icon ,
data: { url: payload.fcmOptions?.link || "/" }, // The notification will redirect to the homepage("/") when clicked. Fell free to redirect to whatever page you want to.
};
// Display push notification
self.registration.showNotification(notificationTitle, notificationOptions);
});
// Handle notification click events
self.addEventListener("notificationclick", (event) => {
event.notification.close();
const targetUrl = event.notification.data?.url || "/";
event.waitUntil(
clients.matchAll({ type: "window", includeUncontrolled: true }) /* Get all open browser tabs controlled by this service worker */
.then((clientList) => {
// Loop through each open tab/window
for (const client of clientList) {
if (client.url.includes(targetUrl) && "focus" in client) {
return client.focus(); // If a matching tab exists, bring it to the front
}
}
// If no matching tab is found, open a new one
return clients.openWindow(targetUrl);
})
);
});
Ask for notification permission
/lib/firebase/requestNotificationPermission.js
// Purpose: Request notification permission, retrieve FCM token and save it
import { getToken, messaging, } from "./firebaseConfig";
import { saveFcmToken } from "@/actions/notification/firebase/saveFcmToken";
export const requestNotificationPermission = async () => {
try {
const permission = await Notification.requestPermission() // Request notification permission
if (permission !== "granted") {
console.warn("Notification permission denied.");
return;
}
// Register the service worker from firebase-messaging-sw.js
const registration = await navigator.serviceWorker.register("/firebase-messaging-sw.js")
console.log("Service Worker registered:", registration);
// Request FCM token for the browser/device & link to the service worker
const token = await getToken(messaging, {
vapidKey: process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY,
serviceWorkerRegistration: registration // Link to service worker, allowing FCM to deliver notifications properly
})
if (!token) {
console.warn("Failed to retrieve FCM token.");
return;
}
// Save the token to supabase
const res = await saveFcmToken(token)
if (!res.success) {
console.error("Error saving token:", res.error);
return
}
} catch (error) {
console.error('Error requesting permission:', error)
}
}
- Create a component that will call the requestNotificationPermission function.
/utils/notification/firebase/NotificationPermission.jsx
"use client";
import { useEffect } from "react";
import { usePathname } from "next/navigation";
import { requestNotificationPermission } from "@/lib/firebase/requestNotificationPermission";
const NotificationPermission = () => {
// Ask notification permission only if user is a student
const pathname = usePathname();
useEffect(() => {
if (pathname.includes("student")) {
const subscribeUser = async () => {
await requestNotificationPermission();
};
subscribeUser();
}
}, [pathname]);
return null; // No UI, just runs logic
};
export default NotificationPermission;
- Add the component (NotificationPermission) to the homepage of your project or whenever you would like the notification permission to be asked from.
/app/layout.js
import "../styles/globals.css"
import NotificationPermission from "@/utils/notification/firebase/NotificationPermission"
const RootLayout = ({ children }) => {
return (
<html lang="en">
<body>
{ children }
<NotificationPermission/>
</body>
</html>
)
}
export default RootLayout
Store FCM token
- Let's now create the saveFcmToken file. We previously called this function when permission was granted inside requestNotificationPermission. Here, we will use next.js Server Actions with Subapase to store the FCM token. However, feel free to use your preferred storage solution.
/actions/notification/firebase/saveFcmToken.js
'use server'
import { createClient } from "@/lib/supabase/server"
// Purpose: Save the retrieved FCM token into Supabase database
export const saveFcmToken = async (token) => {
const supabase = await createClient()
const { error } = await supabase
.from('push_subscriptions')
.upsert({ endpoint: token });
if (error) {
console.log('Error saving FCM token:', error)
return { success: false, error: error.message}
}
return{ success: true, message: 'FCM Token saved successfully' }
}
Trigger notification
- Now, what remains is an event to trigger the pushing of notifications.
Since pushing of notifications happens in the server-side, we will start by creating a firebaseAdmin file to enables server-side interactions.
Install firebase admin sdk
npm i firebase-admin
.
/lib/firebase/firebaseAdmin.js
// Purpose: Initialize Firebase Admin SDK for the backend (Server-Side)
import admin from 'firebase-admin'
const serviceAccount = {
type: "service_account",
project_id: process.env.FIREBASE_PROJECT_ID,
private_key: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, '\n'), // ".replace()" Converts escaped "\n" back into actual newlines for proper parsing
client_email: process.env.FIREBASE_CLIENT_EMAIL,
}
// Ensure that firebase is initialized once
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;
- Create the function to send notifications
/lib/firebase/sendNotification.js
import admin from "@/lib/firebase/firebaseAdmin";
// Purpose: Send push notification when an event occurs in Supabase
export const sendNotification = async (title, body, recipients) => {
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)
}
}
- At this point, you can call sendNotification function whenever there is a need for notifying users. Think of real-world scenarios where notifying users is necessary. Keep in mind that this must be done on the server-side.
- For example, a blog platform may need to notify the users whenever there is a new blog, it would look like this:
/actions/blog/create-blog
'use server'
export async function createNewBlog() {
// Your logic for creating new blog here
// Get subscribers' tokens (the ones we store using _saveToken()_)
// Notify the users of the new blog
await sendNotification("New content", "A new blog has been
posted. Check it out", tokens)
}
- And just like that, our push notification is good to go.
- This implementation works fine both for development and production environment.
Recap of what we did
In this post, we walked through the implementation of Firebase Cloud Messaging (FCM) in a Next.js + Supabase app. Hereβs a quick summary of what we covered:
- Setting Up FCM β Configured Firebase, generated an API keys and created .env.local file.
- Registering User Devices β Stored push notification tokens in Supabase when users subscribed.
- Sending Push Notifications β Implemented a server-side function to send notifications via the FCM HTTP v1 API.
- Handling Notifications in the Client β We set up event listeners to handle background notifications.
Handling Failures
While this implementation covers the essentials, real-world applications should account for token expiration and invalidation(which would have been overwhelming and heavy to include it in this post).
Expired or Invalid Tokens
- FCM tokens may expire or become invalid if a user reinstalls the app, logs out, or disables notifications.
- If an FCM response returns a 410 (NotRegistered) error, the token should be removed from the database.
Permission Revocation
- Users might disable notifications in browser settings.
- Implement a way to prompt re-enabling if necessary.
For more details on handling failures, check out firebase documentation on token management.
Here comes the end
Iβd love to hear your thoughts on this postβdrop them in the comments section below!
Follow me to get a π "push notification" whenever I publish something new!
Cheers ππ
Top comments (1)
great content