Skip to main content
RapidDev - Software Development Agency
firebase-tutorial

How to Subscribe a User to an FCM Topic in Firebase

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.

What you'll learn

  • How to get a user's FCM token on the client side for topic subscription
  • How to subscribe and unsubscribe tokens to topics using the Admin SDK
  • How to send notifications to all subscribers of a topic
  • How to manage topic memberships and handle token refresh
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner10 min read15-20 minFirebase Cloud Messaging (Blaze plan recommended), firebase-admin v12+, firebase/messaging v9+March 2026RapidDev Engineering Team
TL;DR

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

1

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.

typescript
1import { getMessaging, getToken, onMessage } from "firebase/messaging";
2import { initializeApp } from "firebase/app";
3
4const app = initializeApp({ /* your config */ });
5const messaging = getMessaging(app);
6
7async 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 }
13
14 const token = await getToken(messaging, {
15 vapidKey: "your-vapid-key-from-firebase-console",
16 });
17
18 console.log("FCM token:", token);
19 return token;
20}
21
22// Listen for foreground messages
23onMessage(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.

2

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.

typescript
1// Client-side: send token to server
2async 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}
10
11// Server-side: Cloud Function (v2)
12import { onRequest } from "firebase-functions/v2/https";
13import { getMessaging } from "firebase-admin/messaging";
14import { initializeApp } from "firebase-admin/app";
15
16initializeApp();
17const messaging = getMessaging();
18
19export const subscribe = onRequest(async (req, res) => {
20 const { token, topic } = req.body;
21
22 if (!token || !topic) {
23 res.status(400).json({ error: "token and topic are required" });
24 return;
25 }
26
27 // Validate topic name: alphanumeric, hyphens, underscores, dots
28 if (!/^[a-zA-Z0-9_.-]+$/.test(topic)) {
29 res.status(400).json({ error: "Invalid topic name" });
30 return;
31 }
32
33 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.

3

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.

typescript
1import { getMessaging } from "firebase-admin/messaging";
2
3const messaging = getMessaging();
4
5// Send a notification message to a topic
6async 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 };
29
30 const messageId = await messaging.send(message);
31 console.log("Sent to topic:", topic, "messageId:", messageId);
32 return messageId;
33}
34
35// Usage
36await 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.

4

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.

typescript
1// Server-side: unsubscribe endpoint
2import { onRequest } from "firebase-functions/v2/https";
3import { getMessaging } from "firebase-admin/messaging";
4
5const messaging = getMessaging();
6
7export const unsubscribe = onRequest(async (req, res) => {
8 const { token, topic } = req.body;
9
10 if (!token || !topic) {
11 res.status(400).json({ error: "token and topic are required" });
12 return;
13 }
14
15 const result = await messaging.unsubscribeFromTopic([token], topic);
16 res.json({
17 success: result.successCount,
18 errors: result.errors,
19 });
20});
21
22// Track subscriptions in Firestore for UI state
23import { getFirestore, FieldValue } from "firebase-admin/firestore";
24
25const db = getFirestore();
26
27async function updateSubscription(
28 userId: string,
29 topic: string,
30 subscribed: boolean
31) {
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.

5

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.

typescript
1import { getMessaging, getToken } from "firebase/messaging";
2
3const messaging = getMessaging();
4
5// Check for token refresh on each app load
6async function refreshTokenAndResubscribe(userId: string) {
7 const currentToken = await getToken(messaging, {
8 vapidKey: "your-vapid-key",
9 });
10
11 // Compare with stored token
12 const response = await fetch("/api/check-token", {
13 method: "POST",
14 headers: { "Content-Type": "application/json" },
15 body: JSON.stringify({ userId, token: currentToken }),
16 });
17
18 const { tokenChanged, topics } = await response.json();
19
20 if (tokenChanged && topics.length > 0) {
21 // Re-subscribe new token to all user's topics
22 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}
32
33// Server-side: batch re-subscribe
34// 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

fcm-topics.ts
1// Firebase Cloud Messaging — Topic Subscription Management
2// 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";
7
8initializeApp();
9const messaging = getMessaging();
10const db = getFirestore();
11
12// Subscribe to a topic
13export const subscribeTopic = onCall(async (request) => {
14 if (!request.auth) {
15 throw new HttpsError("unauthenticated", "Login required");
16 }
17
18 const { token, topic } = request.data;
19 if (!token || !topic) {
20 throw new HttpsError("invalid-argument", "token and topic required");
21 }
22
23 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 }
30
31 return { success: result.successCount, errors: result.errors };
32});
33
34// Unsubscribe from a topic
35export const unsubscribeTopic = onCall(async (request) => {
36 if (!request.auth) {
37 throw new HttpsError("unauthenticated", "Login required");
38 }
39
40 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 }
47
48 return { success: result.successCount, errors: result.errors };
49});
50
51// Send notification to a topic
52export const notifyTopic = onCall(async (request) => {
53 if (!request.auth) {
54 throw new HttpsError("unauthenticated", "Login required");
55 }
56
57 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 });
67
68 return { messageId };
69});
70
71// Re-subscribe after token refresh
72export const refreshToken = onCall(async (request) => {
73 if (!request.auth) {
74 throw new HttpsError("unauthenticated", "Login required");
75 }
76
77 const { token } = request.data;
78 const userDoc = await db.collection("users").doc(request.auth.uid).get();
79 const userData = userDoc.data();
80
81 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 }
89
90 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.

ChatGPT Prompt

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.

Firebase Prompt

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.