# How to Subscribe an User to an FCM Topic in Firebase

- Tool: Firebase
- Difficulty: Beginner
- Time required: 15-20 min
- Compatibility: Firebase Cloud Messaging (Blaze plan recommended), firebase-admin v12+, firebase/messaging v9+
- Last updated: March 2026

## 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.

## Before you start

- 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.

```
import { getMessaging, getToken, onMessage } from "firebase/messaging";
import { initializeApp } from "firebase/app";

const app = initializeApp({ /* your config */ });
const messaging = getMessaging(app);

async function requestNotificationPermission(): Promise<string | null> {
  const permission = await Notification.requestPermission();
  if (permission !== "granted") {
    console.log("Notification permission denied");
    return null;
  }

  const token = await getToken(messaging, {
    vapidKey: "your-vapid-key-from-firebase-console",
  });

  console.log("FCM token:", token);
  return token;
}

// Listen for foreground messages
onMessage(messaging, (payload) => {
  console.log("Message received:", payload);
});
```

**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.

```
// Client-side: send token to server
async function subscribeToTopic(token: string, topic: string) {
  const response = await fetch("/api/subscribe", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ token, topic }),
  });
  return response.json();
}

// Server-side: Cloud Function (v2)
import { onRequest } from "firebase-functions/v2/https";
import { getMessaging } from "firebase-admin/messaging";
import { initializeApp } from "firebase-admin/app";

initializeApp();
const messaging = getMessaging();

export const subscribe = onRequest(async (req, res) => {
  const { token, topic } = req.body;

  if (!token || !topic) {
    res.status(400).json({ error: "token and topic are required" });
    return;
  }

  // Validate topic name: alphanumeric, hyphens, underscores, dots
  if (!/^[a-zA-Z0-9_.-]+$/.test(topic)) {
    res.status(400).json({ error: "Invalid topic name" });
    return;
  }

  const result = await messaging.subscribeToTopic([token], topic);
  res.json({
    success: result.successCount,
    errors: result.errors,
  });
});
```

**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.

```
import { getMessaging } from "firebase-admin/messaging";

const messaging = getMessaging();

// Send a notification message to a topic
async function sendTopicNotification(
  topic: string,
  title: string,
  body: string,
  data?: Record<string, string>
) {
  const message = {
    topic,
    notification: {
      title,
      body,
    },
    data: data || {},
    webpush: {
      notification: {
        icon: "/icon-192x192.png",
        badge: "/badge-72x72.png",
      },
      fcmOptions: {
        link: "https://your-app.com/notifications",
      },
    },
  };

  const messageId = await messaging.send(message);
  console.log("Sent to topic:", topic, "messageId:", messageId);
  return messageId;
}

// Usage
await sendTopicNotification(
  "product-updates",
  "New Feature Released",
  "Check out our latest update with improved performance.",
  { url: "/changelog" }
);
```

**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.

```
// Server-side: unsubscribe endpoint
import { onRequest } from "firebase-functions/v2/https";
import { getMessaging } from "firebase-admin/messaging";

const messaging = getMessaging();

export const unsubscribe = onRequest(async (req, res) => {
  const { token, topic } = req.body;

  if (!token || !topic) {
    res.status(400).json({ error: "token and topic are required" });
    return;
  }

  const result = await messaging.unsubscribeFromTopic([token], topic);
  res.json({
    success: result.successCount,
    errors: result.errors,
  });
});

// Track subscriptions in Firestore for UI state
import { getFirestore, FieldValue } from "firebase-admin/firestore";

const db = getFirestore();

async function updateSubscription(
  userId: string,
  topic: string,
  subscribed: boolean
) {
  const userRef = db.collection("users").doc(userId);
  if (subscribed) {
    await userRef.update({ topics: FieldValue.arrayUnion(topic) });
  } else {
    await userRef.update({ topics: FieldValue.arrayRemove(topic) });
  }
}
```

**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.

```
import { getMessaging, getToken } from "firebase/messaging";

const messaging = getMessaging();

// Check for token refresh on each app load
async function refreshTokenAndResubscribe(userId: string) {
  const currentToken = await getToken(messaging, {
    vapidKey: "your-vapid-key",
  });

  // Compare with stored token
  const response = await fetch("/api/check-token", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ userId, token: currentToken }),
  });

  const { tokenChanged, topics } = await response.json();

  if (tokenChanged && topics.length > 0) {
    // Re-subscribe new token to all user's topics
    await fetch("/api/resubscribe", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        token: currentToken,
        topics,
      }),
    });
  }
}

// Server-side: batch re-subscribe
// import { getMessaging } from "firebase-admin/messaging";
// const messaging = getMessaging();
// for (const topic of topics) {
//   await messaging.subscribeToTopic([token], topic);
// }
```

**Expected result:** When a user's FCM token changes, the new token is automatically re-subscribed to all their previously selected topics.

## Complete code example

File: `fcm-topics.ts`

```typescript
// Firebase Cloud Messaging — Topic Subscription Management
// Server-side (Cloud Functions v2)
import { initializeApp } from "firebase-admin/app";
import { getMessaging } from "firebase-admin/messaging";
import { getFirestore, FieldValue } from "firebase-admin/firestore";
import { onCall, HttpsError } from "firebase-functions/v2/https";

initializeApp();
const messaging = getMessaging();
const db = getFirestore();

// Subscribe to a topic
export const subscribeTopic = onCall(async (request) => {
  if (!request.auth) {
    throw new HttpsError("unauthenticated", "Login required");
  }

  const { token, topic } = request.data;
  if (!token || !topic) {
    throw new HttpsError("invalid-argument", "token and topic required");
  }

  const result = await messaging.subscribeToTopic([token], topic);
  if (result.successCount > 0) {
    await db.collection("users").doc(request.auth.uid).update({
      topics: FieldValue.arrayUnion(topic),
      fcmToken: token,
    });
  }

  return { success: result.successCount, errors: result.errors };
});

// Unsubscribe from a topic
export const unsubscribeTopic = onCall(async (request) => {
  if (!request.auth) {
    throw new HttpsError("unauthenticated", "Login required");
  }

  const { token, topic } = request.data;
  const result = await messaging.unsubscribeFromTopic([token], topic);
  if (result.successCount > 0) {
    await db.collection("users").doc(request.auth.uid).update({
      topics: FieldValue.arrayRemove(topic),
    });
  }

  return { success: result.successCount, errors: result.errors };
});

// Send notification to a topic
export const notifyTopic = onCall(async (request) => {
  if (!request.auth) {
    throw new HttpsError("unauthenticated", "Login required");
  }

  const { topic, title, body, data } = request.data;
  const messageId = await messaging.send({
    topic,
    notification: { title, body },
    data: data || {},
    webpush: {
      notification: { icon: "/icon-192.png" },
      fcmOptions: { link: "/" },
    },
  });

  return { messageId };
});

// Re-subscribe after token refresh
export const refreshToken = onCall(async (request) => {
  if (!request.auth) {
    throw new HttpsError("unauthenticated", "Login required");
  }

  const { token } = request.data;
  const userDoc = await db.collection("users").doc(request.auth.uid).get();
  const userData = userDoc.data();

  if (userData?.fcmToken !== token && userData?.topics?.length > 0) {
    for (const topic of userData.topics) {
      await messaging.subscribeToTopic([token], topic);
    }
    await db.collection("users").doc(request.auth.uid).update({
      fcmToken: token,
    });
  }

  return { resubscribed: userData?.topics?.length || 0 };
});
```

## Common mistakes

- **Trying to subscribe to topics from the client-side JavaScript SDK — topic management is only available in the Admin SDK** — undefined Fix: Send the FCM token to your server (Cloud Function or backend) and use the Admin SDK's messaging.subscribeToTopic() to manage subscriptions.
- **Not handling FCM token refresh, causing users to silently stop receiving notifications when their token changes** — undefined Fix: Call getToken() on every app load, compare with the stored token, and re-subscribe the new token to all topics if it changed.
- **Using spaces or special characters in topic names, which causes subscription to fail silently** — undefined Fix: 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'.
- **Not storing topic subscriptions in Firestore, making it impossible to display the user's current preferences or re-subscribe after token refresh** — undefined Fix: 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

## 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.

---

Source: https://www.rapidevelopers.com/firebase-tutorial/how-to-subscribe-user-to-fcm-topic
© RapidDev — https://www.rapidevelopers.com/firebase-tutorial/how-to-subscribe-user-to-fcm-topic
