Firebase Cloud Messaging (FCM) topics let you send notifications to groups of users who subscribe to a named topic. Subscribe devices on the client with getToken() and subscribe server-side using the Admin SDK's subscribeToTopic(). Send notifications to a topic from a Cloud Function using admin.messaging().send() with the topic field. Topics are ideal for broadcast messages like news alerts, feature updates, or category-based notifications.
Sending Topic Notifications with Firebase Cloud Messaging
FCM topics provide a publish-subscribe model for notifications. Instead of tracking individual device tokens, you subscribe users to named topics and send messages to all subscribers at once. This tutorial covers setting up FCM, subscribing devices to topics via the Admin SDK, sending notification and data messages to topics from Cloud Functions, using topic conditions for complex targeting, and handling received notifications on the web client.
Prerequisites
- A Firebase project on the Blaze plan (for Cloud Functions)
- Firebase Cloud Messaging enabled in your project
- Firebase Admin SDK installed in your Cloud Functions
- A web app with the Firebase JS SDK configured and notification permissions granted
Step-by-step guide
Get the device registration token on the client
Get the device registration token on the client
Before subscribing a user to a topic, you need their FCM registration token. On the web, request notification permission and call getToken() from the firebase/messaging module. This returns a unique token for the browser instance. Store this token in Firestore linked to the user's UID so your server can manage subscriptions. The token can change when the user clears browser data or the service worker updates, so refresh it on each app load.
1import { getMessaging, getToken } from 'firebase/messaging';2import { getFirestore, doc, setDoc } from 'firebase/firestore';3import { getAuth } from 'firebase/auth';45const messaging = getMessaging();6const db = getFirestore();78async function requestNotificationPermission() {9 const permission = await Notification.requestPermission();10 if (permission !== 'granted') {11 console.log('Notification permission denied');12 return null;13 }1415 const token = await getToken(messaging, {16 vapidKey: 'YOUR_VAPID_KEY'17 });1819 // Store token in Firestore for server-side subscription20 const user = getAuth().currentUser;21 if (user && token) {22 await setDoc(doc(db, 'fcmTokens', user.uid), {23 token,24 updatedAt: new Date()25 });26 }2728 return token;29}Expected result: The browser returns an FCM registration token that is stored in Firestore for server-side use.
Subscribe users to topics from the server
Subscribe users to topics from the server
Topic subscriptions are managed server-side using the Firebase Admin SDK. Call messaging().subscribeToTopic() with an array of registration tokens and a topic name. Topics are created automatically when the first token subscribes. Use Cloud Functions to manage subscriptions, for example by triggering on user preferences changes in Firestore. Topic names can contain letters, numbers, underscores, and hyphens.
1import { initializeApp } from 'firebase-admin/app';2import { getMessaging } from 'firebase-admin/messaging';3import { getFirestore } from 'firebase-admin/firestore';4import { onDocumentCreated } from 'firebase-functions/v2/firestore';5import { logger } from 'firebase-functions/v2';67initializeApp();8const messaging = getMessaging();9const db = getFirestore();1011// Subscribe a user to a topic when they update their preferences12export const manageTopicSubscription = onDocumentCreated(13 'userPreferences/{userId}',14 async (event) => {15 const userId = event.params.userId;16 const prefs = event.data?.data();1718 // Get the user's FCM token from Firestore19 const tokenDoc = await db.collection('fcmTokens').doc(userId).get();20 const token = tokenDoc.data()?.token;2122 if (!token) {23 logger.warn('No FCM token found for user', { userId });24 return;25 }2627 // Subscribe to selected topics28 const topics = prefs?.subscribedTopics || [];29 for (const topic of topics) {30 await messaging.subscribeToTopic([token], topic);31 logger.info('Subscribed to topic', { userId, topic });32 }33 }34);Expected result: User devices are subscribed to the specified FCM topics and will receive messages sent to those topics.
Send a notification to a topic
Send a notification to a topic
Use the Admin SDK's messaging().send() method with a topic field to deliver a notification to all subscribers. The message can include a notification payload (shown in the system tray) and a data payload (processed by your app code). Create an HTTP callable function that your admin dashboard can call to send topic notifications.
1import { onCall, HttpsError } from 'firebase-functions/v2/https';2import { getMessaging } from 'firebase-admin/messaging';3import { logger } from 'firebase-functions/v2';45const messaging = getMessaging();67export const sendTopicNotification = onCall(async (request) => {8 // Only allow admin users to send notifications9 if (!request.auth?.token?.admin) {10 throw new HttpsError('permission-denied', 'Admin access required');11 }1213 const { topic, title, body, data } = request.data;1415 const message = {16 topic: topic,17 notification: {18 title: title,19 body: body20 },21 data: data || {},22 webpush: {23 notification: {24 icon: '/icon-192.png',25 click_action: 'https://your-app.com/notifications'26 }27 }28 };2930 try {31 const response = await messaging.send(message);32 logger.info('Topic notification sent', { topic, messageId: response });33 return { success: true, messageId: response };34 } catch (error: any) {35 logger.error('Failed to send topic notification', {36 topic,37 error: error.message38 });39 throw new HttpsError('internal', 'Failed to send notification');40 }41});Expected result: All devices subscribed to the topic receive the notification.
Use topic conditions for complex targeting
Use topic conditions for complex targeting
FCM supports condition expressions to target users subscribed to combinations of topics. Conditions use logical operators: && (AND), || (OR), and ! (NOT). For example, send a notification to users subscribed to 'sports' AND 'news' but NOT 'promotions'. Conditions support up to 5 topics in a single expression.
1import { getMessaging } from 'firebase-admin/messaging';23const messaging = getMessaging();45// Send to users subscribed to 'sports' AND 'news'6async function sendConditionalNotification() {7 const message = {8 condition: "'sports' in topics && 'news' in topics",9 notification: {10 title: 'Sports News Update',11 body: 'Check out the latest sports headlines'12 }13 };1415 await messaging.send(message);16}1718// Send to 'deals' subscribers who are NOT in 'opted-out'19async function sendDealsNotification() {20 const message = {21 condition: "'deals' in topics && !('opted-out' in topics)",22 notification: {23 title: 'New Deal Available',24 body: 'A new exclusive deal has been posted'25 }26 };2728 await messaging.send(message);29}Expected result: Only users matching the topic condition receive the notification.
Handle incoming notifications on the client
Handle incoming notifications on the client
Set up a message handler on the client to process incoming notifications when the app is in the foreground. For background notifications, configure a service worker. Foreground notifications are not automatically displayed in the system tray, so you need to show them manually using the Notification API or an in-app notification component.
1import { getMessaging, onMessage } from 'firebase/messaging';23const messaging = getMessaging();45// Handle foreground notifications6onMessage(messaging, (payload) => {7 console.log('Foreground notification:', payload);89 // Show browser notification manually10 if (payload.notification) {11 new Notification(payload.notification.title || 'New message', {12 body: payload.notification.body,13 icon: '/icon-192.png'14 });15 }1617 // Handle data payload18 if (payload.data) {19 console.log('Data payload:', payload.data);20 // Update your app state based on the data21 }22});2324// For background notifications, add this to firebase-messaging-sw.js:25// importScripts('https://www.gstatic.com/firebasejs/10.12.0/firebase-app-compat.js');26// importScripts('https://www.gstatic.com/firebasejs/10.12.0/firebase-messaging-compat.js');27// firebase.initializeApp({ ... });28// const messaging = firebase.messaging();29// messaging.onBackgroundMessage((payload) => {30// self.registration.showNotification(payload.notification.title, {31// body: payload.notification.body32// });33// });Expected result: Foreground notifications are displayed in the app and background notifications appear in the system tray.
Complete working example
1// Cloud Function: Send topic notifications (functions/src/notifications.ts)2import { initializeApp } from 'firebase-admin/app';3import { getMessaging } from 'firebase-admin/messaging';4import { getFirestore } from 'firebase-admin/firestore';5import { onCall, HttpsError } from 'firebase-functions/v2/https';6import { onDocumentWritten } from 'firebase-functions/v2/firestore';7import { logger } from 'firebase-functions/v2';89initializeApp();10const messaging = getMessaging();11const db = getFirestore();1213// Subscribe user to topics based on preferences14export const syncTopicSubscriptions = onDocumentWritten(15 'userPreferences/{userId}',16 async (event) => {17 const userId = event.params.userId;18 const before = event.data?.before?.data();19 const after = event.data?.after?.data();2021 const tokenDoc = await db.collection('fcmTokens').doc(userId).get();22 const token = tokenDoc.data()?.token;23 if (!token) return;2425 const oldTopics: string[] = before?.topics || [];26 const newTopics: string[] = after?.topics || [];2728 // Unsubscribe from removed topics29 const removed = oldTopics.filter(t => !newTopics.includes(t));30 for (const topic of removed) {31 await messaging.unsubscribeFromTopic([token], topic);32 logger.info('Unsubscribed from topic', { userId, topic });33 }3435 // Subscribe to new topics36 const added = newTopics.filter(t => !oldTopics.includes(t));37 for (const topic of added) {38 await messaging.subscribeToTopic([token], topic);39 logger.info('Subscribed to topic', { userId, topic });40 }41 }42);4344// Send notification to a topic (admin only)45export const sendTopicNotification = onCall(async (request) => {46 if (!request.auth?.token?.admin) {47 throw new HttpsError('permission-denied', 'Admin required');48 }4950 const { topic, title, body, link } = request.data;51 const response = await messaging.send({52 topic,53 notification: { title, body },54 data: { link: link || '/' },55 webpush: {56 fcmOptions: { link: link || '/' },57 notification: { icon: '/icon-192.png' }58 }59 });6061 logger.info('Notification sent', { topic, messageId: response });62 return { messageId: response };63});Common mistakes when sending Notifications to a Topic in Firebase
Why it's a problem: Trying to subscribe to topics from the client SDK on the web
How to avoid: Web clients cannot subscribe to topics directly. Use the Firebase Admin SDK server-side to call subscribeToTopic() with the client's registration token. Mobile SDKs (iOS/Android) support client-side topic subscription.
Why it's a problem: Not handling token refresh, leading to stale tokens in topic subscriptions
How to avoid: FCM tokens can change. Listen for token refresh events and update the stored token in Firestore. Re-subscribe the new token to the same topics.
Why it's a problem: Sending notifications without checking if the user has admin permissions
How to avoid: Always verify the caller's permissions before sending topic notifications. Use custom claims (auth.token.admin) or Firestore role checks in your Cloud Function.
Best practices
- Manage topic subscriptions server-side with the Admin SDK for consistent state management
- Store FCM tokens in Firestore linked to user UIDs for easy server-side access
- Use topic conditions for targeting intersections and exclusions of multiple topics
- Include both notification and data payloads so foreground and background scenarios are handled
- Handle token refresh events and re-subscribe new tokens to the same topics
- Limit topic names to descriptive, lowercase strings with hyphens (like 'sports-news')
- Validate send permissions in Cloud Functions before dispatching notifications
- Log all notification sends with topic name and message ID for debugging delivery issues
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Walk me through implementing Firebase Cloud Messaging topic notifications for a web app. Show me how to get the device token, subscribe to topics server-side with the Admin SDK, send notifications to a topic from a Cloud Function, and handle incoming messages on the client.
Write Firebase Cloud Functions v2 in TypeScript that manage FCM topic subscriptions based on user preferences stored in Firestore, and provide an admin-only callable function to send notifications to topics. Include both notification and data payloads.
Frequently asked questions
How many topics can a single device subscribe to?
A single device can be subscribed to up to 2,000 topics. There is no limit on the number of subscribers per topic. For most applications, a few dozen topics per user is typical.
Is there a limit on how many devices can subscribe to a topic?
No. There is no upper limit on the number of devices subscribed to a single topic. Topics scale automatically to handle millions of subscribers.
Can I send topic notifications from the Firebase Console?
Yes. Go to Firebase Console > Cloud Messaging > New Notification. You can target by topic in the Target section. However, for production use, sending from Cloud Functions via the Admin SDK gives you more control over the payload and conditions.
What is the difference between notification and data messages?
Notification messages (notification payload) are displayed automatically by the system when the app is in the background. Data messages (data payload) are always handled by your app code. You can include both in a single message for full control over foreground and background behavior.
How fast are topic messages delivered?
Topic messages are typically delivered within a few seconds to most subscribers. However, FCM does not guarantee delivery order or exact timing. Large fan-out operations to millions of subscribers may take up to a few minutes to complete.
Can I unsubscribe a user from a topic?
Yes. Use messaging.unsubscribeFromTopic([token], topicName) with the Admin SDK. You should unsubscribe when a user changes their notification preferences or deletes their account.
Can RapidDev help build a notification system with Firebase topics?
Yes. RapidDev can architect and implement a complete notification system with topic management, user preference sync, admin send interface, notification history tracking, and delivery analytics using Firebase Cloud Messaging.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation