To set up Firebase Cloud Messaging (FCM) in a web app, register a Firebase project, add a service worker file to handle background messages, request notification permission from the user with Notification.requestPermission(), retrieve the FCM registration token using getToken(), and listen for incoming messages with onMessage(). The service worker file firebase-messaging-sw.js runs in the background and displays notifications when the app is not in focus.
Setting Up Firebase Cloud Messaging for Web Push Notifications
Firebase Cloud Messaging (FCM) lets you send push notifications to users even when your web app is closed. This tutorial covers the complete setup from Firebase Console configuration through to receiving and displaying notifications in both foreground and background states. You will create a service worker, request user permission, obtain a device token, and handle incoming messages.
Prerequisites
- A Firebase project created in the Firebase Console
- Firebase SDK installed in your project (npm install firebase)
- A web app served over HTTPS (required for service workers and push notifications)
- Basic understanding of JavaScript Promises and async/await
Step-by-step guide
Generate a VAPID key in the Firebase Console
Generate a VAPID key in the Firebase Console
Navigate to your Firebase project in the Firebase Console. Go to Project Settings (gear icon) then Cloud Messaging tab. Scroll down to the Web configuration section and click Generate key pair. This creates a VAPID (Voluntary Application Server Identification) key that authenticates your app with the FCM service. Copy this key because you will use it when requesting the FCM token in your client code.
Expected result: A VAPID key pair appears in the Cloud Messaging settings. Copy the public key for use in your app code.
Initialize Firebase Messaging in your app
Initialize Firebase Messaging in your app
Import and initialize the Firebase app and messaging modules using the modular SDK v9+ syntax. The getMessaging() function returns the messaging instance that you will use to request tokens and listen for messages. Place this initialization code in a dedicated file so you can import the messaging instance throughout your app.
1// src/firebase.ts2import { initializeApp } from 'firebase/app';3import { getMessaging } from 'firebase/messaging';45const firebaseConfig = {6 apiKey: 'YOUR_API_KEY',7 authDomain: 'YOUR_PROJECT.firebaseapp.com',8 projectId: 'YOUR_PROJECT_ID',9 storageBucket: 'YOUR_PROJECT.appspot.com',10 messagingSenderId: 'YOUR_SENDER_ID',11 appId: 'YOUR_APP_ID'12};1314const app = initializeApp(firebaseConfig);15export const messaging = getMessaging(app);Expected result: Firebase app and messaging are initialized and the messaging instance is exported for use in other files.
Create the service worker file for background messages
Create the service worker file for background messages
Create a file named firebase-messaging-sw.js in your public directory (the root of your served files). This service worker runs in the background and handles push notifications when the user is not actively viewing your app. It must import the Firebase messaging compat libraries and call onBackgroundMessage() to display notifications. The file must be named exactly firebase-messaging-sw.js and served from the root path.
1// public/firebase-messaging-sw.js2importScripts('https://www.gstatic.com/firebasejs/10.12.0/firebase-app-compat.js');3importScripts('https://www.gstatic.com/firebasejs/10.12.0/firebase-messaging-compat.js');45firebase.initializeApp({6 apiKey: 'YOUR_API_KEY',7 authDomain: 'YOUR_PROJECT.firebaseapp.com',8 projectId: 'YOUR_PROJECT_ID',9 storageBucket: 'YOUR_PROJECT.appspot.com',10 messagingSenderId: 'YOUR_SENDER_ID',11 appId: 'YOUR_APP_ID'12});1314const messaging = firebase.messaging();1516messaging.onBackgroundMessage((payload) => {17 const notificationTitle = payload.notification?.title || 'New notification';18 const notificationOptions = {19 body: payload.notification?.body || '',20 icon: '/icon-192x192.png'21 };22 self.registration.showNotification(notificationTitle, notificationOptions);23});Expected result: The firebase-messaging-sw.js file is served at the root of your domain and handles background push notifications.
Request notification permission and get the FCM token
Request notification permission and get the FCM token
Before you can send push notifications, the user must grant permission. Call Notification.requestPermission() to show the browser permission prompt. If granted, call getToken() with your VAPID key to get the unique FCM registration token for this browser on this device. Send this token to your backend server so you can target this user with push messages. The token can change over time, so call getToken() on each app load and update your server if it changes.
1// src/notifications.ts2import { getToken, onMessage } from 'firebase/messaging';3import { messaging } from './firebase';45const VAPID_KEY = 'YOUR_VAPID_PUBLIC_KEY';67export async function requestNotificationPermission(): Promise<string | null> {8 const permission = await Notification.requestPermission();910 if (permission !== 'granted') {11 console.warn('Notification permission denied');12 return null;13 }1415 const token = await getToken(messaging, { vapidKey: VAPID_KEY });16 console.log('FCM Token:', token);1718 // Send this token to your backend to store for this user19 // await saveTokenToServer(token);2021 return token;22}Expected result: The browser shows a notification permission prompt. If granted, an FCM token string is returned and logged to the console.
Handle foreground messages with onMessage()
Handle foreground messages with onMessage()
When the user is actively viewing your app, push messages do not automatically show a browser notification. Instead, FCM delivers them to the onMessage() callback where you can display a custom in-app notification, update a badge count, or show a toast. Set up this listener in your app initialization code so it runs as long as the app is open.
1// src/notifications.ts (continued)2export function listenForMessages(): void {3 onMessage(messaging, (payload) => {4 console.log('Foreground message received:', payload);56 const title = payload.notification?.title || 'Notification';7 const body = payload.notification?.body || '';89 // Option 1: Show a browser notification manually10 if (Notification.permission === 'granted') {11 new Notification(title, { body, icon: '/icon-192x192.png' });12 }1314 // Option 2: Show an in-app toast/alert15 // showToast({ title, body });16 });17}Expected result: When a push message arrives while the app is in the foreground, onMessage fires and you can display the notification content however you choose.
Send a test notification from the Firebase Console
Send a test notification from the Firebase Console
Go to the Firebase Console, navigate to Messaging (under Engage), and click New campaign then Notifications. Enter a notification title and body text. Under Target, select your app. Under Scheduling, select Send now. Click Review then Publish. The notification should appear in your browser within a few seconds. If your app is in the foreground, the onMessage callback fires. If the app is in the background or closed, the service worker displays the notification.
Expected result: A push notification appears in your browser, confirming the complete FCM setup is working end to end.
Complete working example
1// Complete Firebase Cloud Messaging setup for web2// Includes initialization, permission request, token retrieval, and message handling34import { initializeApp } from 'firebase/app';5import { getMessaging, getToken, onMessage } from 'firebase/messaging';67const firebaseConfig = {8 apiKey: 'YOUR_API_KEY',9 authDomain: 'YOUR_PROJECT.firebaseapp.com',10 projectId: 'YOUR_PROJECT_ID',11 storageBucket: 'YOUR_PROJECT.appspot.com',12 messagingSenderId: 'YOUR_SENDER_ID',13 appId: 'YOUR_APP_ID'14};1516const app = initializeApp(firebaseConfig);17const messaging = getMessaging(app);1819const VAPID_KEY = 'YOUR_VAPID_PUBLIC_KEY';2021// Request permission and retrieve the FCM token22export async function initializePushNotifications(): Promise<string | null> {23 try {24 const permission = await Notification.requestPermission();2526 if (permission !== 'granted') {27 console.warn('Notification permission was not granted');28 return null;29 }3031 const token = await getToken(messaging, { vapidKey: VAPID_KEY });32 console.log('FCM registration token:', token);3334 // Store token on your server for this user35 // await fetch('/api/save-fcm-token', {36 // method: 'POST',37 // headers: { 'Content-Type': 'application/json' },38 // body: JSON.stringify({ token })39 // });4041 return token;42 } catch (error) {43 console.error('Failed to initialize push notifications:', error);44 return null;45 }46}4748// Listen for messages when the app is in the foreground49export function onForegroundMessage(50 callback: (title: string, body: string) => void51): void {52 onMessage(messaging, (payload) => {53 const title = payload.notification?.title || 'Notification';54 const body = payload.notification?.body || '';55 callback(title, body);56 });57}5859// Check if notifications are supported and permitted60export function getNotificationStatus(): 'granted' | 'denied' | 'default' | 'unsupported' {61 if (!('Notification' in window)) {62 return 'unsupported';63 }64 return Notification.permission;65}Common mistakes when setting up Firebase Cloud Messaging (FCM)
Why it's a problem: Placing firebase-messaging-sw.js inside a subdirectory instead of the public root, causing service worker registration to fail
How to avoid: The service worker file must be served at the root path of your domain (e.g., https://yourapp.com/firebase-messaging-sw.js). Place it in the public/ directory of your project.
Why it's a problem: Calling Notification.requestPermission() on page load without user interaction, causing browsers to silently block the request
How to avoid: Only request permission after a user action such as clicking an Enable Notifications button. Modern browsers require a user gesture for notification permission prompts.
Why it's a problem: Using the modular SDK v9 import syntax inside the service worker file instead of compat imports
How to avoid: Service workers use importScripts() which only supports the compat (v8-style) Firebase SDK. Use firebase-app-compat.js and firebase-messaging-compat.js in the service worker.
Why it's a problem: Not updating the stored FCM token when it changes, causing notifications to stop working
How to avoid: Call getToken() on every app load and compare the result with the stored token. If it differs, update the token on your server.
Best practices
- Request notification permission only after a clear user action, never on initial page load
- Store FCM tokens on your backend server and update them on every app load in case they change
- Use data messages instead of notification messages when you need full control over display in both foreground and background
- Keep the service worker Firebase SDK version in sync with your main app SDK version
- Provide an in-app way for users to opt out of notifications by deleting their token with deleteToken()
- Test notifications in both foreground and background states during development
- Handle the case where the browser does not support the Notifications API or service workers
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I want to add push notifications to my React web app using Firebase Cloud Messaging. Show me the complete setup including the service worker file, VAPID key configuration, permission request, token retrieval, and handling both foreground and background messages.
Set up Firebase Cloud Messaging in a TypeScript web app with the modular v9+ SDK. Include the firebase-messaging-sw.js service worker for background notifications, getToken() with VAPID key, onMessage() for foreground handling, and a function to save the token to the server.
Frequently asked questions
Does Firebase Cloud Messaging work on the Spark (free) plan?
Yes. FCM is completely free on both Spark and Blaze plans with no message volume limits. You only pay if you use Cloud Functions to send messages programmatically.
Why is getToken() returning an error about service worker registration?
The firebase-messaging-sw.js file must be served at the root of your domain. Ensure it is in your public/ directory and accessible at https://yourdomain.com/firebase-messaging-sw.js. Also verify your site is served over HTTPS, as service workers require a secure context.
Can I send FCM messages from the client side?
No. Sending messages requires the Firebase Admin SDK which runs on a server. The client side only receives messages. Use Cloud Functions, your own backend, or the Firebase Console to send messages.
How long does an FCM token last?
FCM tokens do not have a fixed expiration but can change when the user clears browser data, the service worker is updated, or the app is reinstalled. Call getToken() on every app load and update your server when the token changes.
Why are notifications not showing when the app is in the foreground?
By design, FCM does not display browser notifications when the app is in the foreground. Instead, it delivers the payload to the onMessage() callback. You must handle the display yourself, either by showing an in-app toast or manually creating a Notification object.
Can I customize the notification icon and click action?
Yes. In the service worker's onBackgroundMessage callback, pass icon, badge, image, and data with a click_action URL to the showNotification options. For foreground messages, you have full control in the onMessage handler.
Can RapidDev help implement a complete notification system with Firebase?
Yes. RapidDev can build end-to-end notification systems including FCM setup, server-side sending logic, topic management, notification preferences, and delivery tracking.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation