pwa-fcm-bridge
v1.0.0
Published
Zero-config PWA and FCM notification bridge for React.
Maintainers
Readme
pwa-fcm-bridge
Lightweight Firebase Cloud Messaging (FCM) bridge for React + Vite apps
A type-safe library that handles FCM token registration, foreground message suppression, and push notification setup in service workers. Works standalone (FCM only) or alongside vite-plugin-pwa (full PWA + FCM).
Features
- Firebase Cloud Messaging token registration with permission handling
- Prevents duplicate notifications when the app is in the foreground
- Works with or without
vite-plugin-pwa(optional peer dependency) - Automatic mode via
PwaFcmProvideror manual mode viaregisterNotifications() - CLI scaffold:
npx pwa-fcm-initgenerates your service worker file - Full TypeScript support
Installation
npm install pwa-fcm-bridge firebasevite-plugin-pwa is optional. Install it only if you need full PWA features (offline support, install prompt, update detection):
npm install vite-plugin-pwa --save-devPeer dependencies:
react>= 18firebase>= 12.0.0vite-plugin-pwa>= 0.18.0 (optional)
Step 1: Scaffold the service worker
Run the CLI in your project root to generate src/sw.ts:
# FCM only (no PWA features)
npx pwa-fcm-init fcm
# Full PWA + FCM (requires vite-plugin-pwa)
npx pwa-fcm-init pwaStep 2: Tell Vite to build the service worker
Vite only bundles files reachable from the dependency graph. Because src/sw.ts is loaded at runtime (not imported), you must declare it as an explicit entry point.
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
build: {
rollupOptions: {
input: {
main: 'index.html',
sw: 'src/sw.ts',
},
output: {
entryFileNames: '[name].js',
},
},
},
})This produces dist/sw.js, which the browser can load.
If you use vite-plugin-pwa with injectManifest, the plugin handles building sw.ts automatically.
Usage
Approach 1: Automatic (Provider-based)
The provider initializes Firebase on mount and requests notification permission automatically.
// src/main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('/sw.js')
.catch((err) => console.error('[SW] Registration failed:', err))
}
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
)// src/App.tsx
import { PwaFcmProvider } from 'pwa-fcm-bridge'
const firebaseConfig = {
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
appId: import.meta.env.VITE_FIREBASE_APP_ID,
}
export default function App() {
return (
<PwaFcmProvider
config={{
firebaseConfig,
vapidKey: import.meta.env.VITE_FIREBASE_VAPID_KEY,
enabled: true,
onTokenRefresh: async (token) => {
console.log('FCM Token:', token)
},
onMessageReceived: (payload) => {
console.log('Foreground message:', payload)
},
}}
>
<h1>My App</h1>
</PwaFcmProvider>
)
}// src/sw.ts (generated by npx pwa-fcm-init fcm)
/// <reference lib="webworker" />
import { setupPwaPushListeners } from 'pwa-fcm-bridge/sw'
setupPwaPushListeners({
defaultTitle: 'My App',
defaultIcon: '/pwa-192x192.png',
})Approach 2: Manual (registerNotifications)
Use this when you want to register notifications on user action.
import { registerNotifications } from 'pwa-fcm-bridge'
const firebaseConfig = { /* ... */ }
export default function SubscribeButton() {
const handleSubscribe = async () => {
const token = await registerNotifications({
firebaseConfig,
vapidKey: import.meta.env.VITE_FIREBASE_VAPID_KEY,
enabled: true,
onTokenRefresh: async (token) => {
await fetch('/api/tokens', {
method: 'POST',
body: JSON.stringify({ token }),
})
},
})
if (token) {
console.log('Subscribed! Token:', token)
} else {
console.warn('Permission denied or registration failed')
}
}
return <button onClick={handleSubscribe}>Subscribe to notifications</button>
}Optional: PWA update detection
import { usePwaUpdate } from 'pwa-fcm-bridge'
import { useRegisterSW } from 'virtual:pwa-register/react'
export default function App() {
const registerSW = useRegisterSW()
const { needsUpdate, applyUpdate, dismiss } = usePwaUpdate(registerSW)
return (
<>
{needsUpdate && (
<div>
<p>A new version is available.</p>
<button onClick={applyUpdate}>Update now</button>
<button onClick={dismiss}>Later</button>
</div>
)}
</>
)
}Without vite-plugin-pwa, pass null. The hook returns safe no-op behavior.
Reset state (logout/testing)
import { resetPwaState } from 'pwa-fcm-bridge'
await resetPwaState()API reference
PwaFcmConfig
firebaseConfig: FirebaseConfigvapidKey: stringenabled?: boolean(default true)requestPermission?: boolean(default true)serviceWorkerPath?: stringserviceWorkerRegistration?: ServiceWorkerRegistrationonTokenRefresh?: (token: string) => Promise<void> | voidonMessageReceived?: (payload: MessagePayload) => void
registerNotifications(config)
Returns Promise<string | null>.
setupPwaPushListeners({ defaultTitle, defaultIcon })
Call inside src/sw.ts.
usePwaUpdate(registerSW?)
Returns { needsUpdate, isPwaEnabled, applyUpdate, dismiss }.
resetPwaState()
Clears SW caches, FCM IndexedDB, and service worker registrations.
Environment variables
VITE_FIREBASE_API_KEY=...
VITE_FIREBASE_AUTH_DOMAIN=...
VITE_FIREBASE_PROJECT_ID=...
VITE_FIREBASE_STORAGE_BUCKET=...
VITE_FIREBASE_MESSAGING_SENDER_ID=...
VITE_FIREBASE_APP_ID=...
VITE_FIREBASE_VAPID_KEY=...Browser support
Requires Service Worker and Push API support.
License
MIT
