npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@bhaskardey772/fcm-frontend

v1.2.0

Published

Frontend helper for Firebase Cloud Messaging — request permission, get FCM tokens, and handle push notifications without boilerplate

Downloads

381

Readme

@bhaskardey772/fcm-frontend

npm GitHub

Frontend helper for Firebase Cloud Messaging (FCM). Handle browser notification permission, get FCM device tokens, and listen for push messages — without writing any Firebase boilerplate.

Framework-agnostic (works with React, Vue, Svelte, vanilla JS/TS, or any bundler). Ships with TypeScript declarations.


Table of Contents


Installation

Default (bundled)

firebase is bundled inside — nothing else to install.

npm install @bhaskardey772/fcm-frontend
import * as notif from '@bhaskardey772/fcm-frontend';

Use this if your project does not already use firebase.


Slim (already have firebase)

If your project already uses firebase (e.g. for Firestore, Auth, Realtime Database), use the /slim entry to avoid bundling a second copy of firebase into your app.

npm install @bhaskardey772/fcm-frontend firebase
import * as notif from '@bhaskardey772/fcm-frontend/slim';

The /slim entry uses your project's existing firebase — no duplication, no extra bundle weight.

| | Default | Slim | |---|---|---| | Extra install | None | firebase | | Import path | @bhaskardey772/fcm-frontend | @bhaskardey772/fcm-frontend/slim | | firebase in bundle | Yes (bundled in) | No (uses yours) | | Use when | Fresh project | Already using firebase |


Setup

Add the service worker

Copy the service worker that ships with this package into your public/ folder:

cp node_modules/@bhaskardey772/fcm-frontend/firebase-messaging-sw.js public/

The file must be served at the root path /firebase-messaging-sw.js. Vite and Create React App copy everything from public/ to the build root automatically.

No Firebase config needed inside the file. The service worker handles push events using the native Web Push API — it does not depend on the Firebase SDK. Your firebaseConfig is only needed in your app code for token generation.


Usage

Initialize

Call init() once when your app starts. It registers the service worker and sets up Firebase Messaging for token generation and foreground message handling.

import * as notif from '@bhaskardey772/fcm-frontend';
// or: import * as notif from '@bhaskardey772/fcm-frontend/slim';

await notif.init({
  firebaseConfig: {
    apiKey: 'YOUR_API_KEY',
    authDomain: 'YOUR_PROJECT.firebaseapp.com',
    projectId: 'YOUR_PROJECT_ID',
    storageBucket: 'YOUR_PROJECT.firebasestorage.app',
    messagingSenderId: 'YOUR_SENDER_ID',
    appId: 'YOUR_APP_ID',
  },
  vapidKey: 'YOUR_VAPID_KEY',
});

All other functions work exactly the same regardless of which entry you use.

Where to find your credentials:

| Credential | Location | |---|---| | firebaseConfig | Firebase Console → Project Settings → Your apps → Web app | | vapidKey | Firebase Console → Project Settings → Cloud Messaging → Web Push certificates |


Request Permission & Get Token

const token = await notif.requestPermission();

if (token) {
  // Send token to your backend to register this device
  await fetch('/api/subscribe', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ token }),
  });
} else {
  // User denied — do not re-prompt immediately
}
  • Shows the browser's native permission dialog if not yet answered
  • Returns the FCM token string on 'granted', or null on 'denied'

Listen for Foreground Messages

The service worker suppresses the native popup when the app is already visible — the same behaviour as WhatsApp. Use onForegroundMessage if you want to react to an incoming push while the user is in the app (e.g. show an in-app toast, update a badge):

const unsubscribe = notif.onForegroundMessage(({ title, body, data }) => {
  // e.g. show a custom in-app banner instead of a system notification
  console.log('New message while app is open:', title, body, data);
});

// Stop listening when component unmounts
unsubscribe();

Get Token Without Prompting

On app load, silently check if a token already exists from a previous session:

const token = await notif.getDeviceToken();
// Returns token string, or null if permission not yet granted

Unsubscribe / Delete Token

On logout or when the user opts out:

// Save token before deleting
const token = await notif.getDeviceToken();

// 1. Remove from FCM
await notif.deleteToken();

// 2. Remove from your backend
await fetch('/api/unsubscribe', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ token }),
});

Check Permission State

const state = notif.getPermissionState();
// 'granted' | 'denied' | 'default'

if (state === 'denied') {
  // Show guide to re-enable in browser settings
}
if (state === 'default') {
  // Not yet decided — safe to call requestPermission()
}

Framework Examples

React

Create a hook (src/hooks/usePushNotifications.ts):

import { useEffect, useRef, useState } from 'react';
import * as notif from '@bhaskardey772/fcm-frontend';
// or: import * as notif from '@bhaskardey772/fcm-frontend/slim';

const FIREBASE_CONFIG = {
  apiKey: 'YOUR_API_KEY',
  authDomain: 'YOUR_PROJECT.firebaseapp.com',
  projectId: 'YOUR_PROJECT_ID',
  storageBucket: 'YOUR_PROJECT.firebasestorage.app',
  messagingSenderId: 'YOUR_SENDER_ID',
  appId: 'YOUR_APP_ID',
};
const VAPID_KEY = 'YOUR_VAPID_KEY';

export function usePushNotifications() {
  const [token, setToken]           = useState<string | null>(null);
  const [permission, setPermission] = useState(notif.getPermissionState());
  const initialized                 = useRef(false);

  useEffect(() => {
    // useRef guard prevents double-init in React StrictMode
    if (initialized.current) return;
    initialized.current = true;

    notif.init({ firebaseConfig: FIREBASE_CONFIG, vapidKey: VAPID_KEY });

    const unsubscribe = notif.onForegroundMessage(({ title, body }) => {
      // App is visible — SW won't show a popup. Handle in-app here if needed.
      console.log('Foreground message:', title, body);
    });

    return () => unsubscribe();
  }, []);

  async function subscribe() {
    const fcmToken = await notif.requestPermission();
    if (!fcmToken) { setPermission(notif.getPermissionState()); return null; }

    await fetch('/api/subscribe', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ token: fcmToken }),
    });

    setToken(fcmToken);
    setPermission('granted');
    return fcmToken;
  }

  async function unsubscribe() {
    if (!token) return;
    await notif.deleteToken();
    await fetch('/api/unsubscribe', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ token }),
    });
    setToken(null);
    setPermission('default');
  }

  return { token, permission, subscribe, unsubscribe };
}

Use in any component (src/App.tsx):

import { usePushNotifications } from './hooks/usePushNotifications';

export default function App() {
  const { token, permission, subscribe, unsubscribe } = usePushNotifications();

  return (
    <div>
      {permission === 'granted' ? (
        <button onClick={unsubscribe}>Unsubscribe</button>
      ) : (
        <button onClick={subscribe}>Enable Notifications</button>
      )}
      {token && <p>Token: {token.slice(0, 20)}…</p>}
    </div>
  );
}

Vue 3

// composables/usePushNotifications.ts
import { onMounted, onUnmounted, ref } from 'vue';
import * as notif from '@bhaskardey772/fcm-frontend';
// or: import * as notif from '@bhaskardey772/fcm-frontend/slim';

const FIREBASE_CONFIG = { /* ... */ };
const VAPID_KEY = 'YOUR_VAPID_KEY';

export function usePushNotifications() {
  const token      = ref<string | null>(null);
  const permission = ref(notif.getPermissionState());
  let unsubscribe: (() => void) | undefined;

  onMounted(async () => {
    await notif.init({ firebaseConfig: FIREBASE_CONFIG, vapidKey: VAPID_KEY });

    unsubscribe = notif.onForegroundMessage(({ title, body }) => {
      // App is visible — SW won't show a popup. Handle in-app here if needed.
      console.log('Foreground message:', title, body);
    });
  });

  onUnmounted(() => unsubscribe?.());

  async function subscribe() {
    const fcmToken = await notif.requestPermission();
    if (!fcmToken) { permission.value = notif.getPermissionState(); return; }

    await fetch('/api/subscribe', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ token: fcmToken }),
    });

    token.value = fcmToken;
    permission.value = 'granted';
  }

  async function unsubscribeDevice() {
    if (!token.value) return;
    await notif.deleteToken();
    await fetch('/api/unsubscribe', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ token: token.value }),
    });
    token.value = null;
    permission.value = 'default';
  }

  return { token, permission, subscribe, unsubscribeDevice };
}

Vanilla TypeScript

// main.ts
import * as notif from '@bhaskardey772/fcm-frontend';
// or: import * as notif from '@bhaskardey772/fcm-frontend/slim';

const FIREBASE_CONFIG = { /* ... */ };
const VAPID_KEY = 'YOUR_VAPID_KEY';

await notif.init({ firebaseConfig: FIREBASE_CONFIG, vapidKey: VAPID_KEY });

notif.onForegroundMessage(({ title, body }) => {
  // App is visible — SW won't show a popup. Handle in-app here if needed.
  console.log('Foreground message:', title, body);
});

document.getElementById('subscribeBtn')!.addEventListener('click', async () => {
  const token = await notif.requestPermission();
  if (token) {
    await fetch('/api/subscribe', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ token }),
    });
  }
});

document.getElementById('unsubscribeBtn')!.addEventListener('click', async () => {
  const token = await notif.getDeviceToken();
  await notif.deleteToken();
  if (token) {
    await fetch('/api/unsubscribe', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ token }),
    });
  }
});

API Reference

init(options): Promise<void>

Registers the service worker and initialises Firebase Messaging for token generation and foreground message handling. Must be called before any other function.

interface InitOptions {
  firebaseConfig: {
    apiKey: string;
    authDomain: string;
    projectId: string;
    storageBucket: string;
    messagingSenderId: string;
    appId: string;
    measurementId?: string;
  };
  vapidKey: string;
  serviceWorkerPath?: string; // default: '/firebase-messaging-sw.js'
}

requestPermission(): Promise<string | null>

Shows the browser permission dialog. Returns the FCM token, or null if denied.

getDeviceToken(): Promise<string | null>

Returns the current FCM token without prompting. Returns null if permission not granted.

getPermissionState(): 'granted' | 'denied' | 'default'

Returns the current permission state synchronously.

onForegroundMessage(handler): () => void

Fires when a push message arrives while the app tab is open and visible. The service worker suppresses the native popup in this state, so use this to show a custom in-app notification or update UI. Returns an unsubscribe function.

interface IncomingNotification {
  title: string;
  body: string;
  imageUrl: string;
  data: Record<string, string>;
  raw: unknown;
}

deleteToken(): Promise<boolean>

Deletes the FCM token from the browser. Call this on logout or opt-out, then also notify your backend.


How Notifications Work

| App state | What happens | |---|---| | App visible (tab open & focused) | SW suppresses the popup — onForegroundMessage fires for in-app handling | | App in background (tab hidden / minimised) | SW shows native OS popup automatically | | Browser running, tab closed | SW shows native OS popup automatically | | Browser fully closed | Push is queued by the OS push service and delivered when the browser next starts |

The service worker handles push events using the native Web Push API — no Firebase SDK is loaded inside it. Your firebaseConfig is only used in your app code (for token generation and foreground messages). You never need to edit firebase-messaging-sw.js.


TypeScript

No @types/ package needed. Import types directly:

import type { IncomingNotification, InitOptions } from '@bhaskardey772/fcm-frontend';

Service Worker — Supported data Fields

The service worker reads these fields from the data object of your FCM message. All are optional.

| Field | Type | Description | |---|---|---| | title | string | Notification title (fallback if notification.title is absent) | | body | string | Notification body (fallback if notification.body is absent) | | icon | string | URL of the notification icon | | badge | string | URL of the small monochrome badge icon (status bar) | | image | string | URL of a large inline image | | url | string | URL to open when the notification is clicked (default: /) | | clickUrl | string | Alias for url | | tag | string | Groups notifications — a new notification with the same tag replaces the old one | | actions | string | JSON array of action buttons: [{"action":"view","title":"View"}] | | requireInteraction | string | Set to "false" to let the OS auto-dismiss the notification |

Example — sending with action buttons from your backend:

await notif.sendToDevice(token, {
  title: 'New message',
  body: 'You have a new message.',
  data: {
    url: '/messages',
    tag: 'chat',
    actions: JSON.stringify([
      { action: 'reply',    title: 'Reply' },
      { action: 'dismiss',  title: 'Dismiss' },
    ]),
  },
});

Troubleshooting

"Service worker registration failed"

  • The file must be served at exactly /firebase-messaging-sw.js. Check that public/firebase-messaging-sw.js exists and your build tool copies it to the root.
  • Service workers require a secure origin. Use http://localhost in dev, not an IP address.

Token is null after requestPermission()

  • getPermissionState() returning 'denied' means the user blocked notifications. They must re-enable manually in browser settings — you cannot re-prompt programmatically.

Notification not showing when app is open

  • This is intentional. The service worker suppresses popups only when the app tab is the currently focused window (WhatsApp behaviour). If the tab is open but the user switched to another window or tab, the notification will still appear. Use onForegroundMessage for in-app handling.

Notification not showing when tab is closed or browser is in background

  • Open DevTools → Application → Service Workers and confirm the worker is activated with no errors.
  • Try unregistering the SW and reloading — the updated SW activates immediately via skipWaiting.

Notification not showing when browser is fully closed

  • This requires the browser's background process to be running.
  • Chrome: Settings → System → enable "Continue running background apps when Google Chrome is closed".
  • Firefox: about:configdom.push.enabledtrue.

Using /slim but getting "Cannot find module firebase"

  • Make sure firebase is installed in your project: npm install firebase