Push — FCM
Firebase Cloud Messaging for web, iOS, and Android push notifications.
Firebase Cloud Messaging (FCM) delivers push notifications to web, iOS, and Android clients. The Express API sends messages via firebase-admin using stored FCM registration tokens — the same token format and send API for all platforms.
Install
pnpm add firebase-adminInitialize
// src/lib/fcm.ts
import admin from "firebase-admin";
if (!admin.apps.length) {
admin.initializeApp({
credential: admin.credential.cert({
projectId: process.env.FCM_PROJECT_ID!,
clientEmail: process.env.FCM_CLIENT_EMAIL!,
privateKey: process.env.FCM_PRIVATE_KEY!.replace(/\\n/g, "\n"),
}),
});
}
export const messaging = admin.messaging();Store the service account JSON values in the vault — never commit the key file.
Environment
FCM_PROJECT_ID=your-project-id
FCM_CLIENT_EMAIL=firebase-adminsdk@your-project.iam.gserviceaccount.com
FCM_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n..."Send notification
// src/services/push.service.ts
import { messaging } from "../lib/fcm";
interface PushPayload {
title: string;
body: string;
data?: Record<string, string>;
}
export async function sendToDevice(token: string, payload: PushPayload) {
return messaging.send({
token,
notification: { title: payload.title, body: payload.body },
data: payload.data,
webpush: {
notification: {
title: payload.title,
body: payload.body,
icon: "/icons/notification-192.png",
},
fcmOptions: { link: payload.data?.url },
},
apns: { payload: { aps: { sound: "default" } } },
android: { priority: "high" },
});
}
export async function sendToUser(userId: string, payload: PushPayload) {
const tokens = await getDeviceTokens(userId);
if (!tokens.length) return;
const response = await messaging.sendEachForMulticast({
tokens,
notification: { title: payload.title, body: payload.body },
data: payload.data,
});
// Remove stale tokens
response.responses.forEach((res, i) => {
if (res.error?.code === "messaging/registration-token-not-registered") {
removeDeviceToken(tokens[i]);
}
});
return response;
}Device token registration
All platforms register tokens through the same endpoint:
// POST /v1/devices — called by web, iOS, or Android on login
export async function registerDevice(
userId: string,
token: string,
platform: "web" | "ios" | "android",
) {
await DeviceToken.upsert({ userId, token, platform, updatedAt: new Date() });
}| Platform | When to register |
|---|---|
| Web | After user grants notification permission |
| iOS | On login and when FCM rotates the token |
| Android | On login and when FCM rotates the token |
Web client (Next.js)
Install the Firebase client SDK in the frontend app:
pnpm add firebase// lib/firebase.ts
import { initializeApp, getApps } from "firebase/app";
import { getMessaging, getToken, onMessage, isSupported } from "firebase/messaging";
const firebaseConfig = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY!,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN!,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID!,
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID!,
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID!,
};
export const firebaseApp = getApps().length ? getApps()[0] : initializeApp(firebaseConfig);
export async function registerWebPush() {
if (!(await isSupported())) return null;
const messaging = getMessaging(firebaseApp);
const token = await getToken(messaging, {
vapidKey: process.env.NEXT_PUBLIC_FCM_VAPID_KEY!,
serviceWorkerRegistration: await navigator.serviceWorker.register("/firebase-messaging-sw.js"),
});
return token;
}
export function onForegroundMessage(callback: (payload: unknown) => void) {
const messaging = getMessaging(firebaseApp);
return onMessage(messaging, callback);
}Register the token after the user logs in:
const token = await registerWebPush();
if (token) {
await api.post("/v1/devices", { token, platform: "web" });
}Service worker (background web push)
Place at public/firebase-messaging-sw.js:
importScripts("https://www.gstatic.com/firebasejs/10.14.0/firebase-app-compat.js");
importScripts("https://www.gstatic.com/firebasejs/10.14.0/firebase-messaging-compat.js");
firebase.initializeApp({
apiKey: "...",
projectId: "...",
messagingSenderId: "...",
appId: "...",
});
firebase.messaging().onBackgroundMessage((payload) => {
const { title, body } = payload.notification ?? {};
self.registration.showNotification(title ?? "Notification", {
body,
icon: "/icons/notification-192.png",
data: payload.data,
});
});Notification flow
Use FCM when the app or browser tab is backgrounded or closed. Use Socket.io for in-app realtime alerts when the user is actively on the page.
Topic subscriptions (optional)
await messaging.subscribeToTopic([token], "workspace-123");
await messaging.send({ topic: "workspace-123", notification: { title: "Update", body: "..." } });Prefer per-user token delivery for user-specific events. Use topics for broadcast announcements only.