Firebase Cloud Messaging topics let you send notifications to groups of users without managing individual device tokens. Subscribe users server-side with the Admin SDK's messaging.subscribeToTopic() by passing the user's FCM token and a topic name. Client-side, get the token with getToken() and send it to your backend. Users can be subscribed to multiple topics and unsubscribed at any time.
Group Notifications With FCM Topics
FCM topics are a pub/sub mechanism for push notifications. Instead of storing and managing individual device tokens, you subscribe tokens to named topics like 'news', 'deals', or 'team-updates'. When you send a message to a topic, every subscribed device receives it. This tutorial covers getting the FCM token on the client, subscribing it to topics on the server, sending topic messages, and handling token lifecycle.
Prerequisites
- A Firebase project with Cloud Messaging enabled
- Firebase JS SDK initialized in your web app with a firebase-messaging-sw.js service worker
- A server-side environment (Cloud Functions or Node.js server) with the Firebase Admin SDK
- Notification permission granted by the user in the browser
Step-by-step guide
Get the user's FCM token on the client
Get the user's FCM token on the client
Before subscribing to a topic, you need the user's FCM registration token. Call getToken() from the firebase/messaging module after the user grants notification permission. This token identifies the specific browser or device. Store the token and send it to your server for topic subscription. Tokens can change, so listen for refreshes with onMessage.
1import { getMessaging, getToken, onMessage } from "firebase/messaging";2import { initializeApp } from "firebase/app";34const app = initializeApp({ /* your config */ });5const messaging = getMessaging(app);67async function requestNotificationPermission(): Promise<string | null> {8 const permission = await Notification.requestPermission();9 if (permission !== "granted") {10 console.log("Notification permission denied");11 return null;12 }1314 const token = await getToken(messaging, {15 vapidKey: "your-vapid-key-from-firebase-console",16 });1718 console.log("FCM token:", token);19 return token;20}2122// Listen for foreground messages23onMessage(messaging, (payload) => {24 console.log("Message received:", payload);25});Expected result: The user grants notification permission and you have an FCM token string that uniquely identifies their browser.
Send the token to your server and subscribe to a topic
Send the token to your server and subscribe to a topic
The client cannot subscribe itself to a topic — topic management is a server-side operation using the Admin SDK. Send the FCM token to your server (via a Cloud Function or API endpoint), then call messaging.subscribeToTopic() with the token and topic name. You can subscribe a single token or an array of up to 1,000 tokens at once.
1// Client-side: send token to server2async function subscribeToTopic(token: string, topic: string) {3 const response = await fetch("/api/subscribe", {4 method: "POST",5 headers: { "Content-Type": "application/json" },6 body: JSON.stringify({ token, topic }),7 });8 return response.json();9}1011// Server-side: Cloud Function (v2)12import { onRequest } from "firebase-functions/v2/https";13import { getMessaging } from "firebase-admin/messaging";14import { initializeApp } from "firebase-admin/app";1516initializeApp();17const messaging = getMessaging();1819export const subscribe = onRequest(async (req, res) => {20 const { token, topic } = req.body;2122 if (!token || !topic) {23 res.status(400).json({ error: "token and topic are required" });24 return;25 }2627 // Validate topic name: alphanumeric, hyphens, underscores, dots28 if (!/^[a-zA-Z0-9_.-]+$/.test(topic)) {29 res.status(400).json({ error: "Invalid topic name" });30 return;31 }3233 const result = await messaging.subscribeToTopic([token], topic);34 res.json({35 success: result.successCount,36 errors: result.errors,37 });38});Expected result: The server subscribes the FCM token to the specified topic. The successCount in the response confirms the subscription.
Send a notification to all topic subscribers
Send a notification to all topic subscribers
Use the Admin SDK's messaging.send() with a topic field to deliver a notification to every device subscribed to that topic. You can send notification messages (show a system notification), data messages (handled in your app code), or both. Topic messages support all FCM features including images, click actions, and platform-specific overrides.
1import { getMessaging } from "firebase-admin/messaging";23const messaging = getMessaging();45// Send a notification message to a topic6async function sendTopicNotification(7 topic: string,8 title: string,9 body: string,10 data?: Record<string, string>11) {12 const message = {13 topic,14 notification: {15 title,16 body,17 },18 data: data || {},19 webpush: {20 notification: {21 icon: "/icon-192x192.png",22 badge: "/badge-72x72.png",23 },24 fcmOptions: {25 link: "https://your-app.com/notifications",26 },27 },28 };2930 const messageId = await messaging.send(message);31 console.log("Sent to topic:", topic, "messageId:", messageId);32 return messageId;33}3435// Usage36await sendTopicNotification(37 "product-updates",38 "New Feature Released",39 "Check out our latest update with improved performance.",40 { url: "/changelog" }41);Expected result: Every device subscribed to the topic receives the notification. The function returns a message ID confirming delivery to FCM.
Unsubscribe a user from a topic
Unsubscribe a user from a topic
Users should be able to manage their notification preferences. Call messaging.unsubscribeFromTopic() with the token and topic name to remove the subscription. Build a preferences UI that shows which topics the user is subscribed to and lets them toggle each one. Track subscriptions in Firestore so your UI reflects the current state.
1// Server-side: unsubscribe endpoint2import { onRequest } from "firebase-functions/v2/https";3import { getMessaging } from "firebase-admin/messaging";45const messaging = getMessaging();67export const unsubscribe = onRequest(async (req, res) => {8 const { token, topic } = req.body;910 if (!token || !topic) {11 res.status(400).json({ error: "token and topic are required" });12 return;13 }1415 const result = await messaging.unsubscribeFromTopic([token], topic);16 res.json({17 success: result.successCount,18 errors: result.errors,19 });20});2122// Track subscriptions in Firestore for UI state23import { getFirestore, FieldValue } from "firebase-admin/firestore";2425const db = getFirestore();2627async function updateSubscription(28 userId: string,29 topic: string,30 subscribed: boolean31) {32 const userRef = db.collection("users").doc(userId);33 if (subscribed) {34 await userRef.update({ topics: FieldValue.arrayUnion(topic) });35 } else {36 await userRef.update({ topics: FieldValue.arrayRemove(topic) });37 }38}Expected result: The user is unsubscribed from the topic and stops receiving notifications for that topic.
Handle token refresh and re-subscribe to topics
Handle token refresh and re-subscribe to topics
FCM tokens can change when the user clears browser data, reinstalls the app, or when the browser refreshes the token periodically. When the token changes, the old token's topic subscriptions are lost. Detect token changes and re-subscribe the new token to all the user's topics. Store the current token in Firestore so you can compare and detect changes.
1import { getMessaging, getToken } from "firebase/messaging";23const messaging = getMessaging();45// Check for token refresh on each app load6async function refreshTokenAndResubscribe(userId: string) {7 const currentToken = await getToken(messaging, {8 vapidKey: "your-vapid-key",9 });1011 // Compare with stored token12 const response = await fetch("/api/check-token", {13 method: "POST",14 headers: { "Content-Type": "application/json" },15 body: JSON.stringify({ userId, token: currentToken }),16 });1718 const { tokenChanged, topics } = await response.json();1920 if (tokenChanged && topics.length > 0) {21 // Re-subscribe new token to all user's topics22 await fetch("/api/resubscribe", {23 method: "POST",24 headers: { "Content-Type": "application/json" },25 body: JSON.stringify({26 token: currentToken,27 topics,28 }),29 });30 }31}3233// Server-side: batch re-subscribe34// import { getMessaging } from "firebase-admin/messaging";35// const messaging = getMessaging();36// for (const topic of topics) {37// await messaging.subscribeToTopic([token], topic);38// }Expected result: When a user's FCM token changes, the new token is automatically re-subscribed to all their previously selected topics.
Complete working example
1// Firebase Cloud Messaging — Topic Subscription Management2// Server-side (Cloud Functions v2)3import { initializeApp } from "firebase-admin/app";4import { getMessaging } from "firebase-admin/messaging";5import { getFirestore, FieldValue } from "firebase-admin/firestore";6import { onCall, HttpsError } from "firebase-functions/v2/https";78initializeApp();9const messaging = getMessaging();10const db = getFirestore();1112// Subscribe to a topic13export const subscribeTopic = onCall(async (request) => {14 if (!request.auth) {15 throw new HttpsError("unauthenticated", "Login required");16 }1718 const { token, topic } = request.data;19 if (!token || !topic) {20 throw new HttpsError("invalid-argument", "token and topic required");21 }2223 const result = await messaging.subscribeToTopic([token], topic);24 if (result.successCount > 0) {25 await db.collection("users").doc(request.auth.uid).update({26 topics: FieldValue.arrayUnion(topic),27 fcmToken: token,28 });29 }3031 return { success: result.successCount, errors: result.errors };32});3334// Unsubscribe from a topic35export const unsubscribeTopic = onCall(async (request) => {36 if (!request.auth) {37 throw new HttpsError("unauthenticated", "Login required");38 }3940 const { token, topic } = request.data;41 const result = await messaging.unsubscribeFromTopic([token], topic);42 if (result.successCount > 0) {43 await db.collection("users").doc(request.auth.uid).update({44 topics: FieldValue.arrayRemove(topic),45 });46 }4748 return { success: result.successCount, errors: result.errors };49});5051// Send notification to a topic52export const notifyTopic = onCall(async (request) => {53 if (!request.auth) {54 throw new HttpsError("unauthenticated", "Login required");55 }5657 const { topic, title, body, data } = request.data;58 const messageId = await messaging.send({59 topic,60 notification: { title, body },61 data: data || {},62 webpush: {63 notification: { icon: "/icon-192.png" },64 fcmOptions: { link: "/" },65 },66 });6768 return { messageId };69});7071// Re-subscribe after token refresh72export const refreshToken = onCall(async (request) => {73 if (!request.auth) {74 throw new HttpsError("unauthenticated", "Login required");75 }7677 const { token } = request.data;78 const userDoc = await db.collection("users").doc(request.auth.uid).get();79 const userData = userDoc.data();8081 if (userData?.fcmToken !== token && userData?.topics?.length > 0) {82 for (const topic of userData.topics) {83 await messaging.subscribeToTopic([token], topic);84 }85 await db.collection("users").doc(request.auth.uid).update({86 fcmToken: token,87 });88 }8990 return { resubscribed: userData?.topics?.length || 0 };91});Common mistakes when subscribing a User to an FCM Topic in Firebase
Why it's a problem: Trying to subscribe to topics from the client-side JavaScript SDK — topic management is only available in the Admin SDK
How to avoid: Send the FCM token to your server (Cloud Function or backend) and use the Admin SDK's messaging.subscribeToTopic() to manage subscriptions.
Why it's a problem: Not handling FCM token refresh, causing users to silently stop receiving notifications when their token changes
How to avoid: Call getToken() on every app load, compare with the stored token, and re-subscribe the new token to all topics if it changed.
Why it's a problem: Using spaces or special characters in topic names, which causes subscription to fail silently
How to avoid: Topic names must match the pattern [a-zA-Z0-9_.~%-]+. Use hyphens or underscores instead of spaces. For example, use 'product-updates' not 'product updates'.
Why it's a problem: Not storing topic subscriptions in Firestore, making it impossible to display the user's current preferences or re-subscribe after token refresh
How to avoid: Track each user's subscribed topics in a Firestore document (e.g., users/{uid}.topics array) and update it on every subscribe/unsubscribe call.
Best practices
- Always subscribe and unsubscribe from topics server-side using the Admin SDK, never from client code
- Store each user's topic subscriptions in Firestore to power the preferences UI and handle token refresh
- Validate topic names on the server before passing them to subscribeToTopic — reject invalid characters
- Call getToken() on every app load and compare with the stored token to detect refresh
- Use meaningful topic names that map to user-facing preferences (e.g., 'weekly-digest', 'price-alerts')
- Batch subscribe up to 1,000 tokens at once using the array form of subscribeToTopic for efficiency
- Include both notification and data payloads so the message works in foreground and background
- Provide a clear notification preferences UI where users can opt in and out of each topic
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I need to implement topic-based push notifications in my Firebase web app. Users should be able to subscribe to topics like 'news', 'deals', and 'product-updates' from a preferences page. Write the client-side code to get the FCM token and the server-side Cloud Functions to subscribe, unsubscribe, send topic notifications, and handle token refresh. Store subscriptions in Firestore.
Create Firebase Cloud Functions for FCM topic subscription management. Include an onCall function for subscribing a token to a topic, one for unsubscribing, one for sending a notification to a topic, and one for re-subscribing after token refresh. Store topic memberships in Firestore users collection and validate topic names.
Frequently asked questions
Can I subscribe to topics from the client-side JavaScript SDK?
No. Topic subscription and management are only available through the Firebase Admin SDK, which runs server-side. The client gets the FCM token with getToken() and sends it to your server, where the Admin SDK handles the subscription.
How many topics can a single device be subscribed to?
A single FCM registration token can be subscribed to up to 2,000 topics. This is more than enough for most applications.
Is there a limit on the number of subscribers per topic?
There is no hard limit on subscribers per topic. FCM topics are designed to scale to millions of subscribers. However, very large topics may experience slight delivery delays.
Does FCM topic messaging cost money?
FCM is free for all message types including topic messages. There is no per-message charge. The only cost is if you use Cloud Functions to send messages, which incurs the standard Cloud Functions billing.
What happens to topic subscriptions when the FCM token changes?
When the token changes (browser data cleared, app reinstall, periodic refresh), all topic subscriptions for the old token are lost. You must detect the token change and re-subscribe the new token to the user's topics.
Can I send to multiple topics at once with conditions?
Yes. Use the condition field instead of topic to target combinations: "'news' in topics && 'premium' in topics" sends to users subscribed to both. You can combine up to 5 topics with AND (&&) and OR (||) operators.
Can RapidDev help implement push notifications with FCM topics?
Yes. RapidDev can set up your FCM infrastructure including topic subscription management, notification preferences UI, Cloud Functions for sending, token refresh handling, and Firestore integration for tracking subscriptions.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation