# How to Send Push Notifications with Firebase

- Tool: Firebase
- Difficulty: Intermediate
- Time required: 20-25 min
- Compatibility: Firebase JS SDK v9+, Firebase Admin SDK, FCM (all plans, Blaze for Cloud Functions)
- Last updated: March 2026

## TL;DR

Send push notifications with Firebase Cloud Messaging (FCM) by setting up a service worker on the web client to receive messages, requesting notification permission to get a device token, and sending messages from a Cloud Function or the Admin SDK using that token. FCM supports three message types: notification messages (handled by the OS), data messages (handled by your app), and combined messages. Use topics to send to groups of users and the Admin SDK for server-side sending.

## Sending Push Notifications with Firebase Cloud Messaging

Firebase Cloud Messaging (FCM) lets you send push notifications to web and mobile devices for free with no message limits. This tutorial covers the end-to-end flow: setting up FCM in your web app, requesting permission and obtaining device tokens, sending messages server-side with the Admin SDK, and handling notifications in both foreground and background states. You will also learn how to use topics for group messaging.

## Before you start

- A Firebase project with Cloud Messaging enabled
- Firebase JS SDK v9+ installed in your web app
- A web app key pair generated in Firebase Console (Project Settings > Cloud Messaging > Web Push certificates)
- Firebase Cloud Functions initialized (for server-side sending)

## Step-by-step guide

### 1. Set up the Firebase Messaging service worker

FCM requires a service worker file named firebase-messaging-sw.js in your public root directory. This file handles background notifications when your app is not in the foreground. The service worker initializes Firebase and registers the messaging service. Without this file, background notifications will not work.

```
// public/firebase-messaging-sw.js
importScripts('https://www.gstatic.com/firebasejs/10.12.0/firebase-app-compat.js')
importScripts('https://www.gstatic.com/firebasejs/10.12.0/firebase-messaging-compat.js')

firebase.initializeApp({
  apiKey: 'YOUR_API_KEY',
  authDomain: 'YOUR_PROJECT.firebaseapp.com',
  projectId: 'YOUR_PROJECT_ID',
  messagingSenderId: 'YOUR_SENDER_ID',
  appId: 'YOUR_APP_ID'
})

const messaging = firebase.messaging()

messaging.onBackgroundMessage((payload) => {
  console.log('Background message:', payload)
  const { title, body, icon } = payload.notification ?? {}
  self.registration.showNotification(title ?? 'New notification', {
    body: body ?? '',
    icon: icon ?? '/icon-192.png'
  })
})
```

**Expected result:** The service worker registers on page load and handles background notifications when the app is not in the active tab.

### 2. Request notification permission and get the device token

Before sending notifications to a user, you must request their permission and obtain a device token. Use the Notification API to request permission, then call getToken() from Firebase Messaging with your VAPID key to get the token. Store this token in Firestore linked to the user's UID so you can target notifications to specific users.

```
import { getMessaging, getToken } from 'firebase/messaging'
import { getFirestore, doc, setDoc } from 'firebase/firestore'
import { getAuth } from 'firebase/auth'

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

async function requestNotificationPermission() {
  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'
  })

  // Store the token linked to the user
  const user = getAuth().currentUser
  if (user && token) {
    await setDoc(
      doc(db, 'fcmTokens', user.uid),
      {
        token,
        userId: user.uid,
        updatedAt: new Date(),
        platform: 'web'
      },
      { merge: true }
    )
  }

  console.log('FCM token:', token)
  return token
}
```

**Expected result:** The browser shows a permission dialog. If granted, a device token is returned and stored in Firestore for later use.

### 3. Handle foreground messages in the app

When your app is in the foreground (active tab), FCM delivers messages to your JavaScript code via the onMessage() callback instead of showing a system notification. Handle the message in your app code to show an in-app notification, toast, or badge update. You can optionally show a native notification from the foreground handler.

```
import { getMessaging, onMessage } from 'firebase/messaging'

const messaging = getMessaging()

onMessage(messaging, (payload) => {
  console.log('Foreground message:', payload)

  const { title, body } = payload.notification ?? {}

  // Option 1: Show a native notification manually
  if (Notification.permission === 'granted') {
    new Notification(title ?? 'New message', {
      body: body ?? '',
      icon: '/icon-192.png'
    })
  }

  // Option 2: Show an in-app toast or update UI
  // showToast({ title, body })
})
```

**Expected result:** When a message arrives while the app is open, the callback fires with the full message payload, enabling custom in-app notification handling.

### 4. Send notifications from a Cloud Function

Use the Firebase Admin SDK in a Cloud Function to send notifications server-side. You can send to a specific device token, a topic, or multiple tokens. Create an HTTPS callable function that accepts the target token and message content, then use admin.messaging().send() to deliver the notification.

```
import { onCall, HttpsError } from 'firebase-functions/v2/https'
import { initializeApp } from 'firebase-admin/app'
import { getMessaging } from 'firebase-admin/messaging'
import { getFirestore } from 'firebase-admin/firestore'

initializeApp()

export const sendNotification = onCall(async (request) => {
  if (!request.auth) {
    throw new HttpsError('unauthenticated', 'Must be signed in.')
  }

  const { targetUserId, title, body } = request.data

  // Get the target user's FCM token from Firestore
  const tokenDoc = await getFirestore()
    .doc(`fcmTokens/${targetUserId}`)
    .get()

  if (!tokenDoc.exists) {
    throw new HttpsError('not-found', 'User has no registered device.')
  }

  const { token } = tokenDoc.data()!

  const message = {
    notification: { title, body },
    data: { targetUserId, senderId: request.auth.uid },
    token
  }

  try {
    const response = await getMessaging().send(message)
    return { success: true, messageId: response }
  } catch (error: any) {
    if (error.code === 'messaging/registration-token-not-registered') {
      // Token is stale — delete it
      await getFirestore().doc(`fcmTokens/${targetUserId}`).delete()
    }
    throw new HttpsError('internal', error.message)
  }
})
```

**Expected result:** The Cloud Function sends a push notification to the target user's device and returns the message ID on success.

### 5. Send notifications to a topic for group messaging

Topics let you send a single message to all devices subscribed to that topic, without managing individual tokens. Subscribe users to topics server-side using the Admin SDK, then send messages to the topic. Topics are ideal for broadcast messages like announcements, new content alerts, or category-based notifications.

```
import { getMessaging } from 'firebase-admin/messaging'

const messaging = getMessaging()

// Subscribe a user's token to a topic (server-side)
async function subscribeToTopic(token: string, topic: string) {
  await messaging.subscribeToTopic(token, topic)
}

// Send to all subscribers of a topic
async function sendToTopic(topic: string, title: string, body: string) {
  const message = {
    notification: { title, body },
    topic: topic // e.g., 'news', 'deals', 'product-updates'
  }

  const response = await messaging.send(message)
  console.log('Sent to topic:', response)
  return response
}

// Send to multiple topics with a condition
async function sendToCondition(title: string, body: string) {
  const message = {
    notification: { title, body },
    condition: "'deals' in topics && 'electronics' in topics"
  }

  const response = await messaging.send(message)
  return response
}
```

**Expected result:** All devices subscribed to the specified topic receive the notification. Condition-based sends target users subscribed to multiple topics.

## Complete code example

File: `push-notifications.ts`

```typescript
import { initializeApp } from 'firebase/app'
import { getMessaging, getToken, onMessage, MessagePayload } from 'firebase/messaging'
import { getFirestore, doc, setDoc } from 'firebase/firestore'
import { getAuth } from 'firebase/auth'

const app = initializeApp({
  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!
})

const messaging = getMessaging(app)
const db = getFirestore(app)

export async function initNotifications(): Promise<string | null> {
  const permission = await Notification.requestPermission()
  if (permission !== 'granted') return null

  const token = await getToken(messaging, {
    vapidKey: process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY!
  })

  const user = getAuth().currentUser
  if (user && token) {
    await setDoc(
      doc(db, 'fcmTokens', user.uid),
      { token, userId: user.uid, updatedAt: new Date(), platform: 'web' },
      { merge: true }
    )
  }

  return token
}

export function onForegroundMessage(
  callback: (payload: MessagePayload) => void
) {
  return onMessage(messaging, callback)
}

export async function refreshToken(): Promise<string | null> {
  try {
    const token = await getToken(messaging, {
      vapidKey: process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY!
    })

    const user = getAuth().currentUser
    if (user && token) {
      await setDoc(
        doc(db, 'fcmTokens', user.uid),
        { token, updatedAt: new Date() },
        { merge: true }
      )
    }
    return token
  } catch (error) {
    console.error('Token refresh failed:', error)
    return null
  }
}
```

## Common mistakes

- **Missing the firebase-messaging-sw.js service worker file, causing background notifications to silently fail** — undefined Fix: Create firebase-messaging-sw.js in your public root directory with Firebase initialization and the onBackgroundMessage handler. Without this file, only foreground messages work.
- **Using a stale device token that has been invalidated, causing 'messaging/registration-token-not-registered' errors** — undefined Fix: Handle this error by deleting the stale token from Firestore. Refresh tokens periodically by calling getToken() on app launch and updating the stored value.
- **Not requesting notification permission before calling getToken(), causing a silent failure** — undefined Fix: Always call Notification.requestPermission() first and check that the result is 'granted' before calling getToken(). Browsers require explicit user consent.
- **Storing the VAPID key in the service worker file, which is publicly accessible** — undefined Fix: The VAPID key is safe to expose publicly — it is a public key used for push protocol identification, not a secret. However, keep your Admin SDK credentials (used for sending) strictly server-side.

## Best practices

- Store device tokens in Firestore linked to user UIDs so you can target notifications to specific users
- Refresh tokens on every app launch by calling getToken() and updating Firestore to keep tokens current
- Handle stale tokens by catching 'messaging/registration-token-not-registered' and removing the invalid token from storage
- Use topics for broadcast messaging to avoid managing individual tokens for large user groups
- Include both notification and data payloads so your app can display rich notifications in both foreground and background
- Send notifications from Cloud Functions, never from the client — the Admin SDK credentials must stay server-side
- Request notification permission at a meaningful moment (after sign-up, before a relevant action), not immediately on page load
- Test with the Firebase Console Messaging Composer before writing Cloud Functions to verify your setup works

## Frequently asked questions

### Is Firebase Cloud Messaging free?

Yes. FCM is completely free with no message limits. You can send unlimited notifications to unlimited devices. The only cost is if you use Cloud Functions to send messages (which requires the Blaze plan), but Cloud Functions has a free tier of 2 million invocations per month.

### What is the difference between notification messages and data messages?

Notification messages are handled by the OS and displayed automatically in the system tray when the app is in the background. Data messages are delivered silently to your app code for custom handling. You can combine both in a single message — the notification appears in the background, and the data payload is available when the user taps it.

### Why are my background notifications not showing?

The most common cause is a missing or misconfigured firebase-messaging-sw.js service worker. Verify the file exists at the root of your public directory, contains the correct Firebase config, and is being registered by the browser. Check the browser's Application > Service Workers panel in DevTools.

### How do I send notifications to multiple devices?

Use the Admin SDK's sendEachForMulticast() method with an array of tokens, or use topics to send to all subscribed devices with a single message. Topics are simpler for broadcast scenarios; token arrays give you fine-grained control.

### Do push notifications work on iOS Safari?

Yes, as of iOS 16.4+ and Safari 16.4+. The web app must be added to the Home Screen as a Progressive Web App (PWA). Standard Safari tabs do not support Web Push on iOS. Use a web app manifest and register a service worker.

### Can RapidDev help implement push notifications in my Firebase application?

Yes. RapidDev can build a complete notification system including FCM setup, token management, Cloud Function sending logic, topic-based messaging, and notification UI handling for web and mobile.

---

Source: https://www.rapidevelopers.com/firebase-tutorial/how-to-send-push-notifications-with-firebase
© RapidDev — https://www.rapidevelopers.com/firebase-tutorial/how-to-send-push-notifications-with-firebase
