← Back to list

Complete Guide to Implement Firebase Cloud Messaging (FCM) in NestJS for Web and Mobile

Introduction

Bhesh Raj Neupane · 2026-01-20 05:05 · 51 claps · 6.3 min read
#nest #fcm-push-notification #typescript #push-notification
Open on Medium ↗
Wiki topics: 🌐 · Web Development

Complete Guide to Implement Firebase Cloud Messaging (FCM) in NestJS for Web and Mobile

Introduction

Push notifications are a crucial feature in modern applications. They help deliver real-time updates such as messages, order status, alerts, and promotional notifications. This blog explains how to implement Firebase Cloud Messaging (FCM) using NestJS for both web and mobile devices.

What is Firebase Cloud Messaging (FCM)?

Firebase Cloud Messaging (FCM) is a free service from Google that allows backend servers to send notifications to web browsers, Android devices, and iOS devices.

Technologies Used

  • NestJS (Node.js Framework)
  • Firebase Cloud Messaging
  • Firebase Admin SDK

Architecture Overview

The push notification architecture works in a structured flow where the client application first generates an FCM token using the Firebase SDK. This token uniquely identifies the user’s device and is sent to the NestJS backend after login. The backend securely stores this token and decides when a notification should be sent based on application events. When a notification is triggered, the NestJS server communicates with Firebase using the Firebase Admin SDK, which is authenticated through a service account. Firebase servers then process the request, identify the target devices using the provided tokens, and deliver the notification to the user’s device. This architecture ensures secure communication, keeps sensitive credentials on the backend, and allows reliable and scalable delivery of push notifications across multiple devices.

Step 1: Create Firebase Project

  1. Go to Firebase Console (https://console.firebase.google.com/)

  2. Create a new project

  3. Enable Cloud Messaging

  4. Register Web, Android, and iOS apps

Step 2: Generate Firebase Service Account

Go to Project Settings → Service Accounts → Generate New Private Key.

Download and store the JSON file securely

Important: Never publish your Firebase service account JSON in blogs, GitHub, or Medium. The JSON shown below is only a sample. All sensitive fields, such as the private key, project ID, and other credentials, have been intentionally modified to demonstrate the structure of the file.

{
  "type": "service_account",
  "project_id": "bheshraj-aad9f",
  "private_key_id": "e29801308488e3425berbeab96a1e55fea566e57",
  "private_key": "-----BEGIN PRIVATE KEY-----\nMIG9w0BAQECP5WCKF7rgVmf3\nT+ovkFf24QYyUrlTSZ7QEmb8SIwNAXdfzh54L1pnJWeZHK9DjNxjUKUCDEiAri6N\nsefdXOdX5pzw0h8Z0FSJPrp+UC7urebS2VOQ//I36FlJx4Tdd5yvu8upGEs8H8Po\nP4/WBoHcZ7RK36ZH7qlfWleRXEqUJQZc31aqP4bWND0sEpcP2T3XkzrRuxs8o0jE\nNdZZAwsiDNjjgQz1DEXI9l5We280kDGWrD6XqNZ7gn9XIEXLkXPtEsS1RhMRuqo6\nmPF30Z2HBUEI5Ui8pOXlTOQ2S+eSw4r2GN/dNrhFOdAp2PLegDKUEz4L3Cm7IrxZ\nhY1hiDnDAgMBAAECggEAEQVIzgzQhgiyc4Z4ZPfl78XePcWbNkbXCjWjCORr7H3n\n6OUwpXa0cs7x2iKSeKMIzpaTbhKV4OK5jvkkPALpOnSEy9eO5jbGSRWqaZaCdqkL\nTJV4VQZxWDublCaNiMTFO+QyyuWcwJfhTG94TBCo+7EomlKqEIgicKz2/rKz/6HU\nGrKw7ZPT7Ohyj+8wuYSc9V/Qx9Z85CtZnra67/bek62DLL4NAoE8DsOmNQ/RB5xX\nnruIJdejF9Z6iQUHaLMbw7K0OlJSuwb0XJWt+7ZaMRlvUbWbvD/SnS9ZzQMs30WS\n5/zAs961xDmCvM7K+IK49HIY1Uio/yoElIkgOLBhwQKBgQDFQFqjL6//c0aUAfqb\nsFj4th5rwOfplMzM3d65a/adjbFv7Sh6k/8hEiCbbKFsunhueENY1nH87NzC7s47\n0QaaI/0301mBx93uxceNibCJJUSDQMkzA9w6KAkMSHYqZT8tQVXS5ncYdfxjNnDs\nMtJ+Yn4Xm1U1MsPHLCG/+Xt7NQKBgQC6wOJeZpbgZOXAqir7WYM5+BXC7Ke17U/R\n9R+UHmRzkl9pJuqcZmSjQt2F3LUWC7+MVHbvv6LfYwEROqFII2ifcF3w2DJpOBsZ\n618s7sdTA1E9SBQ+WIBSA3mB7FmB6x/m5rR/VmeYULeD/ajCiFpviCLYhiQ0ePLH\nyM8rqYyIFwKBgEuKC6v29UW01dCUuW6qKEiHJjtJ0wanD49dYJBOAlHwE4Wzow7e\nYpQ9pvMetOs4tipIMDJzXY/o/dpBLgXGVOru4WhhriN/cUShxXw0wMCk3woM44B/\n9/TlpCFqFqbBw2IHZWCxFebuOEuea7xo8ieofHV57TXETSmXgw1L87gJAoGBAKa0\n492s6mXo64bu4Gt63j9zC8nzA0rNSoF7tPK9pqHcObtd9/QhGxj56VFDUYsadaTJ\nCVq+0J9ke2Fr2ujQYuZgohsKgwWWBes/RriPdfLOdUik/R0iV3nejCrEVqo4v0OZ\nEerRsKww8YCrgGgW29PPzHtIUy1cAi0BPp4VPdOFAoGAe7muILgmtNA6OM9X5lnZ\n/6GmAoUVcBCly4ge82kt44P7JQZxiBW9a79sHNiVm8rvPt3lboOtNCzsunxTEjNV\nL8cwgc1JW5qn6tkdmZee3uh51ePTkOXO2gttF+RTeYUyBKuljV99Mjw3hlzOaXJd\nAGsDkfq1uPF4bxqPO6tCXbo=\n-----END PRIVATE KEY-----\n",
  "client_email": "firebase-adminsdk-fbsvc@bheshraj-aad9f.iam.gserviceaccount.com",
  "client_id": "113560538758532085433",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40humanfitcraft-aad9f.iam.gserviceaccount.com",
  "universe_domain": "googleapis.com"
}

Step 3: Initialize and Use Firebase Admin SDK with NestJS

npm install firebase-admin 

After installing the package, we create an FcmNotificationService that implements NestJS OnModuleInit lifecycle hook. This hook runs automatically when the module is loaded, making it the ideal place to initialize Firebase only once. InsideonModuleInit, the service reads the Firebase service account JSON file from the server’s root directory and parses it into an ServiceAccount object. Before initializing Firebase, the code checks whether a default Firebase app already exists, especially one created using Application Default Credentials (ADC), which can cause authentication conflicts. If such an app is found, it is safely deleted to ensure our service-account-based configuration is used. The Firebase Admin SDK is then initialized using the service account credentials and project ID, and a messaging instance is created for sending push notifications. The sendNotification method uses this messaging instance to deliver notifications to multiple devices at once by passing an array of FCM tokens along with the notification title and body. This approach ensures secure initialization, prevents duplicate Firebase apps, and provides a clean, reusable service for sending push notifications across the application.

import {
  Injectable,
  OnModuleInit,
  Logger,

} from '@nestjs/common';
import * as admin from 'firebase-admin';
import { ServiceAccount } from 'firebase-admin';
import * as path from 'path';
import * as fs from 'fs/promises';

@Injectable()
export class FcmNotificationService implements OnModuleInit {
  private readonly logger = new Logger(FcmNotificationService.name);
  private messaging!: admin.messaging.Messaging;
  private app!: admin.app.App;

  constructor() {}

  async onModuleInit() {
    try {
      const serviceAccountPath = path.join(
        process.cwd(),
        'firebase-service-account.json',
      );

      const serviceAccountFile = await fs.readFile(serviceAccountPath, 'utf8');
      const serviceAccount = JSON.parse(serviceAccountFile) as ServiceAccount;

      // Delete any existing default app that might be using ADC
      const defaultApp = admin.apps.find((a) => a.name === '[DEFAULT]');
      if (defaultApp) {
        const credType = (defaultApp.options?.credential as any)?.constructor
          ?.name;
        this.logger.log(
          ` Found existing [DEFAULT] app with credential: ${credType}`,
        );

        if (credType === 'ApplicationDefaultCredential') {
          await defaultApp.delete();
          this.logger.log(' Deleted [DEFAULT] app with ADC credentials');
        }
      }

      if (!admin.apps.length) {
        this.app = admin.initializeApp({
          credential: admin.credential.cert(serviceAccount),
          projectId: serviceAccount.projectId,
        });
        this.logger.log(
          ` Firebase app initialized with projectId: ${serviceAccount.projectId}`,
        );
      } else {
        this.app = admin.apps[0];
        this.logger.log(' Using existing Firebase app');
      }

      this.messaging = this.app.messaging();
      this.logger.log(` Messaging instance ready from app: ${this.app.name}`);
    } catch (error) {
      this.logger.error(
        ` Failed to initialize Firebase Admin SDK: ${(error as Error).message}`,
      );
    }
  }

  async sendNotification(tokens: string[], title: string, body: string) {
    this.logger.log(
      `Attempting to send notification to ${tokens.length} tokens`,
    );

    const message: admin.messaging.MulticastMessage = {
      notification: { title, body },
      tokens,
    };

    try {
      const response = await this.messaging.sendEachForMulticast(message);

      this.logger.log(
        ` Sent fcm notification to ${response.successCount} devices`,
      );

      return { success: true, response };
    } catch (error) {
      this.logger.error(' Error sending fcm notification', error);
      return { success: false, error };
    }
  }
}

The FcmNotificationController manages the registration and removal of FCM device tokens for authenticated users. When a user logs into the application, the client sends the FCM token to the backend through the device/register endpoint. The controller is secured using JwtAuthGuard to ensure only authenticated users can access it, and @AllowRoles(VALID_ROLES.USER) restricts access to users with the correct role. The controller uses the custom decorator @GetUser() to extract the user ID from the JWT token automatically, so the backend can securely associate the device token with the correct user. When a user logs out, the client calls the device/unregister/:token endpoint to remove the token, preventing notifications from being sent to inactive devices.

import {
  Controller,
  Get,
  Post,
  Body,
  Patch,
  Param,
  Delete,
  UseGuards,
  Query,
} from '@nestjs/common';
import { RegisterDeviceDto } from './dto';
import { GetUser, RequestUser } from 'src/common/decorators/get-user.decorator';
import { ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
import { NotificationTokenService } from './notification.token.service';
import { JwtAuthGuard } from 'src/auth/@guard/jwt-auth.guard';
import { RBACGuard } from 'src/auth/@guard/rbac.guard';
import { VALID_ROLES } from 'src/auth/@guard/enums';
import { AllowRoles } from 'src/common/decorators/roles.decorator';

@Controller('fcm-notification')
 @ApiBearerAuth()
 @UseGuards(JwtAuthGuard)
 @AllowRoles(VALID_ROLES.USER)
export class FcmNotificationController {
  constructor(
    private readonly notificationService: NotificationTokenService,
  ) {}

  @Post('device/register')
  @ApiOperation({ summary: 'Register device token for push notifications' })
  registerDevice(
    @Body() registerDeviceDto: RegisterDeviceDto,
    @GetUser() user: RequestUser,
  ) {
    return this.notificationService.registerDevice(user.id, registerDeviceDto);
  }
  @Delete('device/unregister/:token')
  @ApiOperation({ summary: 'Unregister device token' })
  unregisterDevice(
    @Param('token') token: string,
    @GetUser() user: RequestUser,
  ) {
    return this.notificationService.unregisterDevice(user.id, token);
  }

The NotificationTokenService handles the business logic of storing, updating, and deleting device tokens in the database using Prisma. The registerDevice method first checks whether a token already exists — if it does, it updates the user ID, platform, and timestamp; otherwise, it creates a new record. This ensures that tokens remain unique and valid across multiple devices for a single user. The unregisterDevice method validates that the token belongs to the requesting user before deleting it. Together, the controller and service provide a secure and reliable system for managing device tokens, ensuring that push notifications are sent only to active, authenticated users.

import { BadRequestException, Injectable, Logger } from '@nestjs/common';
import { RegisterDeviceDto } from './dto/register-device.dto';
import { PrismaService } from 'src/prisma.service';  

@Injectable()
export class NotificationTokenService {
  private readonly logger = new Logger(NotificationTokenService.name);

  constructor(
    private readonly prismaService: PrismaService,

  ) {}

  /**
   * Register or update a device token for push notifications
   */
  async registerDevice(userId: string, data: RegisterDeviceDto) {
    const existingToken = await this.prismaService.deviceToken.findUnique({
      where: { token: data.token },
    });

    if (existingToken) {
      return this.prismaService.deviceToken.update({
        where: { token: data.token },
        data: {
          userId,
          platform: data.platform,
          updatedAt: new Date(),
        },
      });
    }

    return this.prismaService.deviceToken.create({
      data: {
        userId,
        token: data.token,
        platform: data.platform,
      },
    });
  }

  /**
   * Unregister a device token
   */
  async unregisterDevice(userId: string, token: string) {
    const device = await this.prismaService.deviceToken.findFirst({
      where: { token, userId },
    });

    if (!device) {
      throw new BadRequestException('Device token not found');
    }

    await this.prismaService.deviceToken.delete({
      where: { id: device.id },
    });

    return { success: true };
  }
}

The FcmNotificationModule is a NestJS module that encapsulates all functionality related to push notifications using Firebase Cloud Messaging (FCM). It imports the UserModule because some notification operations, such as associating device tokens with users, require user data. The module registers FcmNotificationController to handle HTTP requests for device token registration and unregistration.

It also provides three main services: FcmNotificationService, which handles communication with Firebase for sending notifications; NotificationTokenService, which manages storing, updating, and deleting device tokens in the database; and PrismaService, which allows database operations. By exporting FcmNotificationService and NotificationTokenServiceOther modules can inject these services to trigger notifications or manage tokens, making the system modular, reusable, and maintainable. You can include FcmNotificationModule in the AppModule to integrate FCM functionality into your application and make push notifications available across your backend services.

import { Module } from '@nestjs/common';
import { FcmNotificationService } from './fcm-notification.service';
import { FcmNotificationController } from './fcm-notification.controller';
import { NotificationTokenService } from './notification.token.service';
import { PrismaService } from 'src/prisma.service';
import { UserModule } from 'src/user/user.module';

@Module({
  imports: [UserModule)],
  controllers: [FcmNotificationController],
  providers: [FcmNotificationService, NotificationTokenService, PrismaService],
  exports: [FcmNotificationService, NotificationTokenService],
})
export class FcmNotificationModule {}

Conclusion

In this article, we built a complete push notification system using NestJS and Firebase Cloud Messaging (FCM). We covered how to securely manage device tokensNotificationTokenService, protect routes, and extract user information using JWT guards and custom decorators, and send notifications to active users through the FcmNotificationService. By organizing the functionality into a dedicatedFcmNotificationModule, the system remains modular, maintainable, and reusable across the application.

This architecture ensures that notifications are delivered only to authenticated, active devices, improving security and user experience. With this setup, developers can now extend the system to support multiple notification types, real-time events, and more advanced features like topic-based notifications or analytics. Implementing FCM in a structured, scalable way not only improves your app’s engagement but also sets a strong foundation for future growth.


메타데이터
post_id
2fbb4df1b0b2
slug
complete-guide-to-implement-firebase-cloud-messaging-fcm-in-nestjs-for-web-and-mobile-2fbb4df1b0b2
url
https://medium.com/@bheshrajneupane/complete-guide-to-implement-firebase-cloud-messaging-fcm-in-nestjs-for-web-and-mobile-2fbb4df1b0b2
canonical_url
https://medium.com/@bheshrajneupane/complete-guide-to-implement-firebase-cloud-messaging-fcm-in-nestjs-for-web-and-mobile-2fbb4df1b0b2
author_url
https://medium.com/@bheshrajneupane
status
ok
fetched_at
2026-06-20 20:29:01