UXDL Docs

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

bash
pnpm add firebase-admin

Initialize

typescript
// 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

dotenv
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

typescript
// 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:

typescript
// 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() });
}
PlatformWhen to register
WebAfter user grants notification permission
iOSOn login and when FCM rotates the token
AndroidOn login and when FCM rotates the token

Web client (Next.js)

Install the Firebase client SDK in the frontend app:

bash
pnpm add firebase
typescript
// 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:

typescript
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:

javascript
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)

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

Official documentation