How to Send Push Notifications from Node.js with Firebase — Part 2: Code Implementation

In Part 1, we set up the Firebase Console — generated the Admin SDK key, enabled Cloud Messaging, and sent a test notification. In this part, we implement the Node.js backend so your server can send push notifications to specific users on demand.
We'll follow an industry-standard directory structure: place the service account JSON securely, add an fcm_token field to your users table, expose an API for the mobile app to register tokens, initialize firebase-admin, and build a reusable notification service that sends messages to one or many devices.
Prerequisites
You must have completed Part 1 — the firebase-service-account.json (or equivalent) file downloaded and your Firebase project ready. You also need a Node.js project with a users table or collection (we'll add an FCM token field to it) and an existing auth flow so you can identify the requesting user when saving the token.
Step 1 — Place the Service Account JSON and Secure It
In your Node.js project root, place the JSON file you downloaded from Firebase (Settings → Service accounts → Generate new private key). Rename it to something clear, e.g. firebase-service-account.json.
your-nodejs-project/
├── firebase-service-account.json ← place here (root)
├── src/
├── .gitignore
└── package.jsonThis file is sensitive — it contains your project's private key. Never commit it to a remote repository. Add it to .gitignore immediately:
# Firebase Admin SDK key (sensitive)
firebase-service-account.json
*-service-account*.jsonIn production, prefer environment variables or a secrets manager (e.g. AWS Secrets Manager, GCP Secret Manager) and load the JSON from there instead of a file on disk.
Step 2 — Add FCM Token to Your Users Model
Each user needs a place to store their FCM device token. The mobile app obtains this token from the Firebase SDK and sends it to your backend; you persist it and use it later when sending notifications.
Edit your users table/document and add a new column or field: fcm_token (or fcmToken if you use camelCase). If you use MongoDB with Mongoose, add the field to your User schema:
// Example: User schema (Mongoose)
const userSchema = new Schema({
email: { type: String, required: true },
name: { type: String },
// ... other fields
fcmToken: { type: String, default: null }, // FCM device token for push notifications
});If you use SQL (e.g. PostgreSQL, MySQL), add a nullable column and run a migration:
-- Example: SQL migration
ALTER TABLE users ADD COLUMN fcm_token VARCHAR(255) NULL;A user may have multiple devices (phone + tablet). To support that, you can store an array of tokens (e.g. fcm_tokens) and update the save/load logic accordingly. For this guide we keep a single fcmToken per user for simplicity.
Step 3 — API Endpoint to Save the FCM Token
The mobile app must send the FCM token to your backend after the user logs in (and whenever the token is refreshed). Expose a protected endpoint that associates the token with the authenticated user.
Example with Express and a user attached via auth middleware (e.g. JWT):
export const saveFCMToken = async (req: Request, res: Response) => {
try {
const userId = req.user?.id;
const { token } = req.body;
const user = await models.User.findById(userId);
if (!user) {
return res.status(404).json({ message: "User not found" });
}
user.fcmToken = token;
await user.save();
return res.status(200).json({ message: "Token saved successfully" });
} catch (error) {
return res.status(500).json({ message: "Internal server error", error });
}
};Register the route (e.g. POST /api/users/fcm-token) and protect it with your auth middleware so only logged-in users can update their token. Validate that req.body.token is a non-empty string before saving.
On the client (React Native), call this API after getting the FCM token from @react-native-firebase/messaging and after login or token refresh.
Step 4 — Install firebase-admin
npm install firebase-admin
# or
yarn add firebase-adminUse the official firebase-admin SDK — it is the only supported way to send messages from a Node.js server.
Step 5 — Initialize Firebase Admin (Config)
Create a config module that initializes the Firebase Admin SDK once and exports the messaging instance. This way you don't re-initialize on every request.
Create src/config/firebase.ts (or src/config/firebase.js):
import admin from "firebase-admin";
import serviceAccount from "../../firebase-service-account.json";
admin.initializeApp({
credential: admin.credential.cert(serviceAccount as admin.ServiceAccount),
});
export const messaging = admin.messaging();If you load credentials from an environment variable (e.g. in production), use:
const serviceAccount = process.env.FIREBASE_SERVICE_ACCOUNT
? JSON.parse(process.env.FIREBASE_SERVICE_ACCOUNT)
: require("../../firebase-service-account.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
});Ensure the JSON path in require() or import matches where you placed the file. If your config lives elsewhere (e.g. src/config/), adjust the relative path (e.g. ../../firebase-service-account.json from src/config/).
Step 6 — Notification Service (Send to One or Many Tokens)
Create a reusable service that sends a notification to a list of FCM tokens. Firebase's sendEachForMulticast sends to multiple tokens and returns per-token success/failure, so you can remove invalid tokens from your database.
Create src/services/notification.service.ts:
import { messaging } from "../config/firebase";
interface SendParams {
tokens: string[];
title: string;
body: string;
data?: Record<string, string>;
}
export async function sendNotification({
tokens,
title,
body,
data = {},
}: SendParams) {
if (!tokens.length) return;
const response = await messaging.sendEachForMulticast({
tokens,
notification: {
title,
body,
},
data: {
...data,
title,
body,
},
android: {
priority: "high" as const,
},
apns: {
payload: {
aps: { sound: "default" },
},
fcmOptions: {
imageUrl: data.imageUrl,
},
},
});
console.log("FCM response", response.successCount, "/", response.responses.length);
return response;
}Important: notification is used for the visible title/body. data is for custom key-value pairs (all values must be strings) that your app can read when the notification is opened. We include title/body in data as well so the app can use them in background/data-only scenarios.
Handle failed tokens: loop over response.responses and where success = false==, remove that token from your user record (e.g. invalid or uninstalled app).
Step 7 — Sending a Notification from Your Code
Wherever you need to send a push (e.g. after an order is placed, or a new message is created), load the user's fcmToken and call the service:
import { sendNotification } from "./services/notification.service";
// Example: after creating an order
const user = await User.findById(order.userId);
if (user?.fcmToken) {
await sendNotification({
tokens: [user.fcmToken],
title: "Order confirmed",
body: `Your order #${order.id} has been placed.`,
data: { orderId: order.id, screen: "OrderDetail" },
});
}For multiple users, collect all non-null fcmToken values and pass them as tokens. Always check for null/empty tokens before calling sendNotification.
File Structure Summary
your-nodejs-project/
├── firebase-service-account.json # Do not commit
├── src/
│ ├── config/
│ │ └── firebase.ts # Initialize admin, export messaging
│ ├── services/
│ │ └── notification.service.ts # sendNotification()
│ ├── controllers/
│ │ └── user.controller.ts # saveFCMToken
│ └── routes/
│ └── user.routes.ts # POST /users/fcm-token
├── .gitignore # Include firebase-service-account.json
└── package.jsonHandling Invalid or Expired Tokens
FCM tokens can become invalid (user uninstalled the app, token rotated, etc.). Use the sendEachForMulticast response to clean up:
const response = await messaging.sendEachForMulticast({ tokens, notification, data });
response.responses.forEach((resp, idx) => {
if (!resp.success && resp.error?.code === "messaging/invalid-registration-token") {
const badToken = tokens[idx];
// Remove badToken from user document(s) in DB
await User.updateMany({ fcmToken: badToken }, { $set: { fcmToken: null } });
}
});Testing End-to-End
1) Run your Node.js server with firebase-service-account.json in place. 2) From your React Native app, get the FCM token and call POST /api/users/fcm-token with it. 3) Trigger a send (e.g. from a test route or after a real action) and confirm the notification appears on the device. 4) For iOS, ensure your APNs key is uploaded in Firebase; for Android, ensure google-services.json is configured.
Recap
You've set up: secure storage of the Firebase service account key, an fcm_token field on users and an API to save the token from the app, Firebase Admin initialization in src/config/firebase.ts, and a sendNotification service that sends to one or many tokens with sendEachForMulticast. Together with Part 1, your Node.js backend can now send push notifications to iOS and Android devices on demand.
Related Articles

How to Send Push Notifications from Node.js with Firebase — Part 1: Firebase Console Setup
Step-by-step guide to configuring Firebase Cloud Messaging for your Node.js backend — generating the Admin SDK key, enabling Cloud Messaging, and sending your first test notification to iOS and Android devices.

How to Register an Android App with Firebase in React Native (2026 Guide)
A complete step-by-step guide to adding Firebase to your React Native Android app — from creating the app in the Firebase Console to configuring Gradle files and verifying the setup.