Push Notification Integration¶
Complete guide for frontend engineers to implement Firebase Cloud Messaging (FCM) push notifications in web and mobile applications.
Overview¶
The push notification system enables real-time notifications to users across web browsers and mobile devices using Firebase Cloud Messaging (FCM).
Key Features:
- Web Push Notifications - Browser notifications via Service Workers
- Cross-Platform Support - Web, Android, iOS compatible
- Foreground & Background - Handle notifications in both states
- Device Registration - Automatic token management and refresh
- Multi-Device Support - Same user, multiple devices
- Automatic Delivery - Triggered by booking events, reminders, payments
Integration Flow:
- Setup Firebase - Configure Firebase SDK in your app
- Register Service Worker - Handle background notifications
- Generate FCM Token - Get unique device token from Firebase
- Register Device - Store token with backend API
- Receive Notifications - Handle foreground and background messages
- Token Refresh - Update token when it changes
Prerequisites¶
Firebase Project Configuration¶
Your app needs these Firebase configuration values (provided by backend team):
const firebaseConfig = {
apiKey: "AIzaSyBRYaAn9jQctShoJ3ZmmYj18p-IFHCkoB8",
authDomain: "skilled-compass-218404.firebaseapp.com",
projectId: "skilled-compass-218404",
storageBucket: "skilled-compass-218404.firebasestorage.app",
messagingSenderId: "740443181568",
appId: "1:740443181568:web:1e975c24ccb34014a7fee5"
};
// VAPID key for web push
const VAPID_KEY = 'BNeGVaRaXsZlb-mqZ2fKQQh3Bpjg72HCdKHLgpBohBjJn1s9d_iXjjSHXqWdsgIXUkuiOY2Lo5Q6IgYa9iAIlow';
Required Packages¶
NPM Installation:
CDN (Browser):
<script type="module">
import { initializeApp } from 'https://www.gstatic.com/firebasejs/10.7.0/firebase-app.js';
import { getMessaging, getToken, onMessage } from 'https://www.gstatic.com/firebasejs/10.7.0/firebase-messaging.js';
</script>
Step 1: Initialize Firebase¶
React/Next.js Setup¶
lib/firebase.js:
import { initializeApp, getApps } from 'firebase/app';
import { getMessaging, getToken, onMessage, isSupported } from 'firebase/messaging';
const firebaseConfig = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID
};
const VAPID_KEY = process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY;
// Initialize Firebase (singleton)
let app;
let messaging;
export function initializeFirebase() {
if (typeof window === 'undefined') return null; // Server-side guard
if (!getApps().length) {
app = initializeApp(firebaseConfig);
} else {
app = getApps()[0];
}
return app;
}
export async function getFirebaseMessaging() {
if (typeof window === 'undefined') return null;
// Check if messaging is supported
const supported = await isSupported();
if (!supported) {
console.warn('Firebase Messaging is not supported in this browser');
return null;
}
if (!messaging) {
const app = initializeFirebase();
messaging = getMessaging(app);
}
return messaging;
}
export { getToken, onMessage, VAPID_KEY };
Vue.js Setup¶
plugins/firebase.js:
import { initializeApp } from 'firebase/app';
import { getMessaging, getToken, onMessage, isSupported } from 'firebase/messaging';
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
};
const VAPID_KEY = import.meta.env.VITE_FIREBASE_VAPID_KEY;
let messaging = null;
export async function initFirebaseMessaging() {
const supported = await isSupported();
if (!supported) return null;
const app = initializeApp(firebaseConfig);
messaging = getMessaging(app);
return messaging;
}
export { messaging, getToken, onMessage, VAPID_KEY };
Step 2: Service Worker Setup¶
The service worker handles background notifications when the app/tab is not focused.
Create Service Worker¶
public/firebase-messaging-sw.js:
// Import Firebase scripts (compat version for service workers)
importScripts('https://www.gstatic.com/firebasejs/10.7.0/firebase-app-compat.js');
importScripts('https://www.gstatic.com/firebasejs/10.7.0/firebase-messaging-compat.js');
// Firebase configuration
firebase.initializeApp({
apiKey: "AIzaSyBRYaAn9jQctShoJ3ZmmYj18p-IFHCkoB8",
authDomain: "skilled-compass-218404.firebaseapp.com",
projectId: "skilled-compass-218404",
storageBucket: "skilled-compass-218404.firebasestorage.app",
messagingSenderId: "740443181568",
appId: "1:740443181568:web:1e975c24ccb34014a7fee5"
});
const messaging = firebase.messaging();
// Handle background messages
messaging.onBackgroundMessage((payload) => {
console.log('[SW] Background message received:', payload);
const notificationTitle = payload.notification?.title || 'New Notification';
const notificationOptions = {
body: payload.notification?.body || '',
icon: '/icons/notification-icon.png',
badge: '/icons/badge-icon.png',
tag: payload.data?.notification_type || 'default',
data: payload.data || {},
vibrate: [200, 100, 200],
requireInteraction: true, // Keep visible until user interacts
actions: [
{ action: 'view', title: 'View Details' },
{ action: 'dismiss', title: 'Dismiss' }
]
};
self.registration.showNotification(notificationTitle, notificationOptions);
});
// Handle notification click
self.addEventListener('notificationclick', (event) => {
event.notification.close();
const action = event.action;
const data = event.notification.data || {};
if (action === 'dismiss') return;
// Determine URL to open based on notification type
let urlToOpen = '/';
switch (data.notification_type) {
case 'booking_confirmation':
case 'booking_reminder':
case 'booking_rescheduled':
urlToOpen = data.appointment_id
? `/appointments/${data.appointment_id}`
: '/appointments';
break;
case 'payment_receipt':
urlToOpen = data.payment_id
? `/payments/${data.payment_id}`
: '/payments';
break;
default:
urlToOpen = data.action_url || '/notifications';
}
event.waitUntil(
clients.matchAll({ type: 'window', includeUncontrolled: true })
.then((clientList) => {
// Focus existing window if available
for (const client of clientList) {
if (client.url.includes(self.location.origin) && 'focus' in client) {
client.postMessage({
type: 'NOTIFICATION_CLICKED',
data: data,
url: urlToOpen
});
return client.focus();
}
}
// Open new window
if (clients.openWindow) {
return clients.openWindow(urlToOpen);
}
})
);
});
// Service worker lifecycle
self.addEventListener('install', () => {
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
event.waitUntil(clients.claim());
});
Register Service Worker¶
In your main app initialization:
async function registerServiceWorker() {
if (!('serviceWorker' in navigator)) {
console.warn('Service Workers not supported');
return null;
}
try {
const registration = await navigator.serviceWorker.register(
'/firebase-messaging-sw.js',
{ scope: '/' }
);
// Wait for service worker to be ready
if (registration.installing) {
await new Promise((resolve) => {
registration.installing.addEventListener('statechange', (e) => {
if (e.target.state === 'activated') resolve();
});
});
}
await navigator.serviceWorker.ready;
console.log('Service Worker registered successfully');
return registration;
} catch (error) {
console.error('Service Worker registration failed:', error);
return null;
}
}
Step 3: Request Notification Permission¶
Permission Request¶
async function requestNotificationPermission() {
if (!('Notification' in window)) {
return { granted: false, reason: 'not_supported' };
}
if (Notification.permission === 'granted') {
return { granted: true, reason: 'already_granted' };
}
if (Notification.permission === 'denied') {
return { granted: false, reason: 'denied' };
}
// Request permission
const permission = await Notification.requestPermission();
return {
granted: permission === 'granted',
reason: permission
};
}
React Hook Example¶
hooks/useNotificationPermission.js:
import { useState, useCallback } from 'react';
export function useNotificationPermission() {
const [permission, setPermission] = useState(
typeof window !== 'undefined' ? Notification.permission : 'default'
);
const [loading, setLoading] = useState(false);
const requestPermission = useCallback(async () => {
setLoading(true);
try {
const result = await Notification.requestPermission();
setPermission(result);
return result === 'granted';
} catch (error) {
console.error('Permission request failed:', error);
return false;
} finally {
setLoading(false);
}
}, []);
return {
permission,
isGranted: permission === 'granted',
isDenied: permission === 'denied',
isDefault: permission === 'default',
requestPermission,
loading
};
}
Step 4: Generate FCM Token¶
Token Generation¶
import { getFirebaseMessaging, getToken, VAPID_KEY } from './firebase';
async function generateFCMToken() {
try {
// Check permission
if (Notification.permission !== 'granted') {
throw new Error('Notification permission not granted');
}
// Get messaging instance
const messaging = await getFirebaseMessaging();
if (!messaging) {
throw new Error('Firebase Messaging not supported');
}
// Register service worker
const registration = await navigator.serviceWorker.ready;
// Get FCM token
const token = await getToken(messaging, {
vapidKey: VAPID_KEY,
serviceWorkerRegistration: registration
});
if (!token) {
throw new Error('Failed to generate FCM token');
}
console.log('FCM Token generated:', token.substring(0, 20) + '...');
return token;
} catch (error) {
console.error('FCM token generation failed:', error);
throw error;
}
}
Complete Initialization Flow¶
async function initializePushNotifications() {
// Step 1: Check/request permission
const { granted, reason } = await requestNotificationPermission();
if (!granted) {
return {
success: false,
error: `Permission ${reason}`,
token: null
};
}
// Step 2: Register service worker
const swRegistration = await registerServiceWorker();
if (!swRegistration) {
return {
success: false,
error: 'Service worker registration failed',
token: null
};
}
// Step 3: Generate FCM token
try {
const token = await generateFCMToken();
return {
success: true,
error: null,
token: token
};
} catch (error) {
return {
success: false,
error: error.message,
token: null
};
}
}
Step 5: Register Device with Backend¶
After obtaining the FCM token, register it with the backend to enable push notification delivery.
Full API Documentation
For complete API documentation including all endpoints, request/response schemas, error handling, and best practices, see Device Management.
Quick Reference¶
| Endpoint | Method | Purpose |
|---|---|---|
/api/v1/devices/register |
POST | Register FCM token (Staff) |
/api/v1/devices/register/customer |
POST | Register FCM token (Customer) |
JavaScript Implementation¶
class PushNotificationService {
constructor(apiBaseUrl, getAuthToken) {
this.apiBaseUrl = apiBaseUrl;
this.getAuthToken = getAuthToken; // Function that returns JWT token
this.fcmToken = null;
this.deviceId = null;
}
async registerDevice(fcmToken, isCustomer = false) {
const endpoint = isCustomer
? '/api/v1/devices/register/customer'
: '/api/v1/devices/register';
const deviceInfo = this.getDeviceInfo();
const response = await fetch(`${this.apiBaseUrl}${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${await this.getAuthToken()}`
},
body: JSON.stringify({
fcm_token: fcmToken,
platform: 'web',
device_name: deviceInfo.deviceName,
app_version: deviceInfo.appVersion,
device_info: deviceInfo
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Device registration failed');
}
const data = await response.json();
this.fcmToken = fcmToken;
this.deviceId = data.id;
// Store token locally for refresh detection
localStorage.setItem('fcm_token', fcmToken);
localStorage.setItem('device_id', data.id);
return data;
}
getDeviceInfo() {
const ua = navigator.userAgent;
return {
deviceName: this.getDeviceName(ua),
appVersion: '1.0.0', // Your app version
browser: this.getBrowserName(ua),
os: this.getOSName(ua),
screen: `${window.screen.width}x${window.screen.height}`,
language: navigator.language,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
};
}
getBrowserName(ua) {
if (ua.includes('Chrome')) return 'Chrome';
if (ua.includes('Firefox')) return 'Firefox';
if (ua.includes('Safari')) return 'Safari';
if (ua.includes('Edge')) return 'Edge';
return 'Unknown';
}
getOSName(ua) {
if (ua.includes('Windows')) return 'Windows';
if (ua.includes('Mac')) return 'macOS';
if (ua.includes('Linux')) return 'Linux';
if (ua.includes('Android')) return 'Android';
if (ua.includes('iOS')) return 'iOS';
return 'Unknown';
}
getDeviceName(ua) {
const browser = this.getBrowserName(ua);
const os = this.getOSName(ua);
return `${browser} on ${os}`;
}
}
Step 6: Handle Incoming Notifications¶
Foreground Notifications¶
When the app is in the foreground (tab is active):
import { onMessage } from 'firebase/messaging';
function setupForegroundNotifications(messaging) {
onMessage(messaging, (payload) => {
console.log('Foreground notification received:', payload);
// Extract notification data
const { notification, data } = payload;
const title = notification?.title || 'New Notification';
const body = notification?.body || '';
// Option 1: Show in-app notification (Toast/Snackbar)
showInAppNotification({
title,
body,
type: data?.notification_type,
data
});
// Option 2: Also show browser notification
if (Notification.permission === 'granted') {
new Notification(title, {
body,
icon: '/icons/notification-icon.png',
tag: data?.notification_type || 'default',
data
});
}
// Option 3: Update notification badge/counter
updateNotificationBadge();
// Option 4: Refresh relevant data
handleNotificationAction(data);
});
}
function handleNotificationAction(data) {
const type = data?.notification_type;
switch (type) {
case 'booking_confirmation':
case 'booking_reminder':
case 'booking_rescheduled':
case 'booking_cancelled':
// Refresh appointments list
refreshAppointments();
break;
case 'payment_receipt':
// Refresh payment history
refreshPayments();
break;
default:
// Refresh notification list
refreshNotifications();
}
}
Background Notifications¶
Background notifications are handled by the service worker (see Step 2).
Listen for Service Worker Messages¶
// Listen for notification clicks from service worker
navigator.serviceWorker.addEventListener('message', (event) => {
if (event.data?.type === 'NOTIFICATION_CLICKED') {
const { data, url } = event.data;
// Handle navigation
if (url) {
window.location.href = url;
}
// Handle specific notification types
handleNotificationClick(data);
}
});
function handleNotificationClick(data) {
const type = data?.notification_type;
switch (type) {
case 'booking_confirmation':
// Navigate to appointment details
router.push(`/appointments/${data.appointment_id}`);
break;
case 'booking_reminder':
// Show appointment reminder modal
showReminderModal(data);
break;
case 'payment_receipt':
// Navigate to receipt
router.push(`/payments/${data.payment_id}`);
break;
}
}
Step 7: Token Refresh Handling¶
FCM tokens can expire or change. Handle token refresh:
import { onMessage } from 'firebase/messaging';
class TokenRefreshHandler {
constructor(pushService) {
this.pushService = pushService;
this.lastToken = localStorage.getItem('fcm_token');
}
async checkAndRefreshToken() {
try {
const currentToken = await generateFCMToken();
if (currentToken !== this.lastToken) {
console.log('FCM token changed, re-registering device...');
await this.pushService.registerDevice(currentToken);
this.lastToken = currentToken;
}
} catch (error) {
console.error('Token refresh check failed:', error);
}
}
// Call periodically (e.g., every hour or on app focus)
startPeriodicCheck(intervalMs = 3600000) {
setInterval(() => this.checkAndRefreshToken(), intervalMs);
// Also check on window focus
window.addEventListener('focus', () => this.checkAndRefreshToken());
}
}
Device Management APIs¶
For complete device management API documentation, see Device Management.
Available Endpoints¶
| Endpoint | Method | Purpose |
|---|---|---|
/devices/register |
POST | Register FCM token (Staff) |
/devices/register/customer |
POST | Register FCM token (Customer) |
/devices |
GET | List user's devices (Staff) |
/devices/customer |
GET | List user's devices (Customer) |
/devices/{id} |
PUT | Update device (Staff) |
/devices/customer/{id} |
PUT | Update device (Customer) |
/devices/{id} |
DELETE | Unregister device (Staff) |
/devices/customer/{id} |
DELETE | Unregister device (Customer) |
The Device Management documentation includes:
- Full request/response schemas
- Error handling and common errors
- Device lifecycle diagrams
- Token refresh handling
- Best practices for registration and logout
- Frontend integration examples (JavaScript, React)
Customer Notification Endpoints¶
Customers can view their notification history and preferences.
Get Customer Notifications¶
Endpoint¶
Authentication: Required (Customer JWT)
Query Parameters¶
| Parameter | Type | Description |
|---|---|---|
status |
string | Filter: unread, read, all |
type |
string | Filter by notification type |
skip |
integer | Pagination offset |
limit |
integer | Results per page (max: 100) |
Response¶
{
"items": [
{
"id": "507f1f77bcf86cd799439020",
"notification_type": "booking_confirmation",
"title": "Booking Confirmed!",
"body": "Your appointment at Salon Cantik has been confirmed",
"is_read": false,
"data": {
"appointment_id": "507f1f77bcf86cd799439030",
"service_names": "Hair Cut, Styling"
},
"created_at": "2025-01-15T10:00:00Z"
}
],
"total": 25,
"unread_count": 5,
"skip": 0,
"limit": 20
}
Mark Notification as Read¶
Endpoint¶
Authentication: Required (Customer JWT)
Response¶
Mark All as Read¶
Endpoint¶
Authentication: Required (Customer JWT)
Response¶
Complete React Implementation¶
Push Notification Provider¶
contexts/PushNotificationContext.js:
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
import { initializeFirebase, getFirebaseMessaging, getToken, onMessage, VAPID_KEY } from '../lib/firebase';
import { useAuth } from './AuthContext';
const PushNotificationContext = createContext(null);
export function PushNotificationProvider({ children }) {
const { token: authToken, isCustomer } = useAuth();
const [permission, setPermission] = useState('default');
const [fcmToken, setFcmToken] = useState(null);
const [deviceId, setDeviceId] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [notifications, setNotifications] = useState([]);
// Check initial permission
useEffect(() => {
if (typeof window !== 'undefined' && 'Notification' in window) {
setPermission(Notification.permission);
}
}, []);
// Initialize push notifications when user logs in
useEffect(() => {
if (authToken && permission === 'granted') {
initializePush();
}
}, [authToken, permission]);
const requestPermission = useCallback(async () => {
if (!('Notification' in window)) {
setError('Notifications not supported');
return false;
}
const result = await Notification.requestPermission();
setPermission(result);
return result === 'granted';
}, []);
const initializePush = useCallback(async () => {
if (!authToken) return;
setLoading(true);
setError(null);
try {
// 1. Register service worker
await navigator.serviceWorker.register('/firebase-messaging-sw.js');
await navigator.serviceWorker.ready;
// 2. Get Firebase messaging
const messaging = await getFirebaseMessaging();
if (!messaging) throw new Error('Messaging not supported');
// 3. Get FCM token
const token = await getToken(messaging, {
vapidKey: VAPID_KEY,
serviceWorkerRegistration: await navigator.serviceWorker.ready
});
if (!token) throw new Error('Failed to get FCM token');
// 4. Register with backend
const endpoint = isCustomer
? '/api/v1/devices/register/customer'
: '/api/v1/devices/register';
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({
fcm_token: token,
platform: 'web',
device_name: `${getBrowser()} on ${getOS()}`
})
});
if (!response.ok) throw new Error('Device registration failed');
const data = await response.json();
setFcmToken(token);
setDeviceId(data.id);
// 5. Setup foreground handler
onMessage(messaging, handleForegroundMessage);
// 6. Listen for SW messages
navigator.serviceWorker.addEventListener('message', handleSWMessage);
} catch (err) {
setError(err.message);
console.error('Push init failed:', err);
} finally {
setLoading(false);
}
}, [authToken, isCustomer]);
const handleForegroundMessage = useCallback((payload) => {
const notification = {
id: Date.now(),
title: payload.notification?.title,
body: payload.notification?.body,
type: payload.data?.notification_type,
data: payload.data,
timestamp: new Date(),
isRead: false
};
setNotifications(prev => [notification, ...prev]);
// Show toast notification
if (window.showToast) {
window.showToast({
title: notification.title,
message: notification.body,
type: 'info'
});
}
}, []);
const handleSWMessage = useCallback((event) => {
if (event.data?.type === 'NOTIFICATION_CLICKED') {
// Handle navigation or action
console.log('Notification clicked:', event.data);
}
}, []);
const unregisterDevice = useCallback(async () => {
if (!deviceId || !authToken) return;
const endpoint = isCustomer
? `/api/v1/devices/customer/${deviceId}`
: `/api/v1/devices/${deviceId}`;
await fetch(`${process.env.NEXT_PUBLIC_API_URL}${endpoint}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${authToken}` }
});
setFcmToken(null);
setDeviceId(null);
}, [deviceId, authToken, isCustomer]);
const value = {
permission,
fcmToken,
deviceId,
loading,
error,
notifications,
isEnabled: permission === 'granted' && !!fcmToken,
requestPermission,
initializePush,
unregisterDevice,
clearNotifications: () => setNotifications([])
};
return (
<PushNotificationContext.Provider value={value}>
{children}
</PushNotificationContext.Provider>
);
}
export function usePushNotifications() {
const context = useContext(PushNotificationContext);
if (!context) {
throw new Error('usePushNotifications must be used within PushNotificationProvider');
}
return context;
}
// Helpers
function getBrowser() {
const ua = navigator.userAgent;
if (ua.includes('Chrome')) return 'Chrome';
if (ua.includes('Firefox')) return 'Firefox';
if (ua.includes('Safari')) return 'Safari';
return 'Browser';
}
function getOS() {
const ua = navigator.userAgent;
if (ua.includes('Windows')) return 'Windows';
if (ua.includes('Mac')) return 'macOS';
if (ua.includes('Linux')) return 'Linux';
return 'Unknown';
}
Notification Bell Component¶
components/NotificationBell.js:
import { useState } from 'react';
import { usePushNotifications } from '../contexts/PushNotificationContext';
export function NotificationBell() {
const {
permission,
isEnabled,
notifications,
requestPermission,
initializePush,
loading
} = usePushNotifications();
const [showDropdown, setShowDropdown] = useState(false);
const unreadCount = notifications.filter(n => !n.isRead).length;
const handleEnableNotifications = async () => {
const granted = await requestPermission();
if (granted) {
await initializePush();
}
};
return (
<div className="notification-bell">
<button
onClick={() => setShowDropdown(!showDropdown)}
className="bell-button"
>
<BellIcon />
{unreadCount > 0 && (
<span className="badge">{unreadCount}</span>
)}
</button>
{showDropdown && (
<div className="dropdown">
{!isEnabled ? (
<div className="enable-prompt">
<p>Enable push notifications to stay updated</p>
<button
onClick={handleEnableNotifications}
disabled={loading || permission === 'denied'}
>
{loading ? 'Enabling...' : 'Enable Notifications'}
</button>
{permission === 'denied' && (
<p className="denied-message">
Notifications blocked. Please enable in browser settings.
</p>
)}
</div>
) : (
<div className="notification-list">
{notifications.length === 0 ? (
<p className="empty">No notifications yet</p>
) : (
notifications.slice(0, 5).map(n => (
<NotificationItem key={n.id} notification={n} />
))
)}
</div>
)}
</div>
)}
</div>
);
}
function NotificationItem({ notification }) {
return (
<div className={`notification-item ${notification.isRead ? '' : 'unread'}`}>
<div className="title">{notification.title}</div>
<div className="body">{notification.body}</div>
<div className="time">{formatTime(notification.timestamp)}</div>
</div>
);
}
Best Practices¶
For Setup & Initialization¶
DO:
- Initialize Firebase only once (singleton pattern)
- Check
isSupported()before using Firebase Messaging - Request permission explicitly with user action (button click)
- Handle all permission states (granted, denied, default)
- Store FCM token locally for refresh detection
- Register service worker before getting FCM token
DON'T:
- Auto-request permission on page load (bad UX, may get denied)
- Assume notifications are supported in all browsers
- Skip service worker registration
- Ignore token refresh handling
- Store sensitive data in notification payloads
For User Experience¶
DO:
- Explain why notifications are useful before requesting
- Provide easy way to enable/disable notifications
- Show in-app notifications for foreground messages
- Use notification badges/counters
- Allow users to manage notification preferences
- Handle notification clicks with appropriate navigation
DON'T:
- Send too many notifications (notification fatigue)
- Use notifications for marketing without consent
- Ignore notification grouping for multiple messages
- Show duplicate notifications (foreground + background)
- Leave stale notifications in the UI
For Security¶
DO:
- Validate authentication before device registration
- Use HTTPS for all API calls
- Clear FCM token on user logout
- Unregister device when user logs out
- Handle token refresh securely
DON'T:
- Store FCM tokens in cookies
- Send sensitive data in notification body
- Trust client-provided user IDs
- Skip authentication for device endpoints
Notification Types Reference¶
| Type | When Triggered | Typical Content |
|---|---|---|
booking_confirmation |
Appointment booked | "Your booking is confirmed for [date] at [time]" |
booking_reminder |
Before appointment | "Reminder: Your appointment is in [X hours]" |
booking_cancelled |
Appointment cancelled | "Your appointment has been cancelled" |
booking_rescheduled |
Time changed | "Your appointment has been rescheduled to [new time]" |
payment_receipt |
Payment completed | "Payment of [amount] received" |
welcome |
Customer registered | "Welcome to [tenant name]!" |
Troubleshooting¶
Common Issues¶
| Issue | Cause | Solution |
|---|---|---|
| "Permission denied" | User blocked notifications | Show instructions to enable in browser settings |
| "Service Worker 404" | Wrong path | Ensure firebase-messaging-sw.js is in public/ folder |
| "No active Service Worker" | SW not ready | Wait for navigator.serviceWorker.ready |
| "Invalid VAPID key" | Wrong key | Use correct VAPID key from Firebase Console |
| Token is null | Permission not granted | Request permission first |
| Notifications not showing | SW not receiving | Check SW console for errors |
| "messaging/token-subscribe-failed" | Invalid credentials | Verify Firebase config |
Debug Checklist¶
- Browser Console:
- Check for Firebase initialization errors
- Verify FCM token is generated
-
Look for service worker errors
-
Service Worker Console:
- Open DevTools → Application → Service Workers
- Check for registration errors
-
Verify background message handler
-
Network Tab:
- Verify device registration API call succeeds
- Check authentication headers
-
Confirm response status
-
Backend Logs:
- Check for FCM send errors
- Verify notification was queued
- Look for token validation failures
Browser Support¶
| Browser | Web Push Support |
|---|---|
| Chrome | Full support |
| Firefox | Full support |
| Edge | Full support |
| Safari | Limited (requires APNS) |
| Opera | Full support |
| Mobile Chrome | Full support |
| Mobile Firefox | Full support |
API Reference Summary¶
Device Management Endpoints¶
See Device Management for full API documentation.
| Endpoint | Method | Auth | Purpose |
|---|---|---|---|
/devices/register |
POST | Staff JWT | Register FCM token (staff) |
/devices/register/customer |
POST | Customer JWT | Register FCM token (customer) |
/devices |
GET | Staff JWT | List staff devices |
/devices/customer |
GET | Customer JWT | List customer devices |
/devices/{id} |
PUT | Staff JWT | Update device |
/devices/customer/{id} |
PUT | Customer JWT | Update customer device |
/devices/{id} |
DELETE | Staff JWT | Unregister device |
/devices/customer/{id} |
DELETE | Customer JWT | Unregister customer device |
Customer Notification Endpoints¶
| Endpoint | Method | Auth | Purpose |
|---|---|---|---|
/customer/notifications |
GET | Customer JWT | Get notification history |
/customer/notifications/{id}/read |
PUT | Customer JWT | Mark as read |
/customer/notifications/read-all |
PUT | Customer JWT | Mark all as read |
Related Documentation¶
| Document | Description |
|---|---|
| Device Management | Complete device/FCM token API documentation |
| Notification Management | Backend notification sending and scheduling |
| Notification Settings | Template customization and preferences |
| Customer Authentication | Customer login and JWT token handling |
| Staff Authentication | Staff login and JWT token handling |
Next Steps:
- Set up Firebase configuration in your app
- Create and register the service worker
- Implement permission request UI
- Add device registration after login
- Handle foreground and background notifications
- Add notification preferences to user settings