Skip to Content
ServicesPush Notification Service

Push Notification Service

Web and mobile push notifications using Firebase Cloud Messaging (FCM) and Web Push API. Send real-time notifications to users across platforms.


Installation & Setup

1. Install Firebase SDK

pnpm add firebase firebase-admin

2. Configure Firebase

Create a Firebase project at console.firebase.google.com 

# .env FIREBASE_PROJECT_ID=your-project-id FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n..." FIREBASE_CLIENT_EMAIL=firebase-adminsdk@your-project.iam.gserviceaccount.com # Public keys for web push NEXT_PUBLIC_FIREBASE_API_KEY=AIzaSyXXXXXXXX NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com NEXT_PUBLIC_FIREBASE_PROJECT_ID=your-project-id NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=123456789 NEXT_PUBLIC_FIREBASE_APP_ID=1:123456789:web:xxxxx NEXT_PUBLIC_VAPID_KEY=BNxxxxxxxxxxxxxx

3. Initialize Firebase Admin

// packages/core/src/services/push/admin.ts import * as admin from 'firebase-admin'; if (!admin.apps.length) { admin.initializeApp({ credential: admin.credential.cert({ projectId: process.env.FIREBASE_PROJECT_ID, privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'), clientEmail: process.env.FIREBASE_CLIENT_EMAIL }) }); } export const messaging = admin.messaging();

4. Initialize Firebase Client

// lib/firebase.ts import { initializeApp } from 'firebase/app'; import { getMessaging } 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, messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID, appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID }; const app = initializeApp(firebaseConfig); export const messaging = getMessaging(app);

3. Usage

The Push Notification Service is available as a singleton via getPushNotificationService.

import { getPushNotificationService } from '@orbitusdev/core/services/push'; const pushService = getPushNotificationService();

Supported Providers

  • Firebase Cloud Messaging (FCM) (Default)

Client-Side Setup

Request Permission

'use client'; import { useEffect, useState } from 'react'; import { getToken, onMessage } from 'firebase/messaging'; import { messaging } from '@/lib/firebase'; export function usePushNotifications() { const [token, setToken] = useState<string | null>(null); const [permission, setPermission] = useState<NotificationPermission>( typeof window !== 'undefined' ? Notification.permission : 'default' ); useEffect(() => { if (typeof window === 'undefined' || !('Notification' in window)) { return; } // Listen for messages when app is in foreground const unsubscribe = onMessage(messaging, (payload) => { console.log('Message received:', payload); // Show notification new Notification(payload.notification?.title ?? 'New Notification', { body: payload.notification?.body, icon: payload.notification?.image ?? '/icon-192.png', badge: '/badge-72.png' }); }); return () => unsubscribe(); }, []); const requestPermission = async () => { try { const permission = await Notification.requestPermission(); setPermission(permission); if (permission === 'granted') { const currentToken = await getToken(messaging, { vapidKey: process.env.NEXT_PUBLIC_VAPID_KEY }); if (currentToken) { setToken(currentToken); // Save token to backend await fetch('/api/notifications/subscribe', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: currentToken }) }); } } } catch (error) { console.error('Error requesting permission:', error); } }; return { token, permission, requestPermission }; }

Notification Component

'use client'; import { usePushNotifications } from '@/hooks/use-push-notifications'; export function NotificationPrompt() { const { permission, requestPermission } = usePushNotifications(); if (permission === 'granted') { return null; } return ( <div className="notification-prompt"> <p>Enable notifications to stay updated!</p> <button onClick={requestPermission}> {permission === 'denied' ? 'Blocked' : 'Enable Notifications'} </button> </div> ); }

Service Worker

Create public/firebase-messaging-sw.js:

importScripts('https://www.gstatic.com/firebasejs/10.7.1/firebase-app-compat.js'); importScripts('https://www.gstatic.com/firebasejs/10.7.1/firebase-messaging-compat.js'); firebase.initializeApp({ apiKey: 'YOUR_API_KEY', authDomain: 'your-project.firebaseapp.com', projectId: 'your-project-id', storageBucket: 'your-project.appspot.com', messagingSenderId: '123456789', appId: '1:123456789:web:xxxxx' }); const messaging = firebase.messaging(); // Handle background messages messaging.onBackgroundMessage((payload) => { console.log('Background message received:', payload); const notificationTitle = payload.notification?.title ?? 'New Notification'; const notificationOptions = { body: payload.notification?.body, icon: payload.notification?.image ?? '/icon-192.png', badge: '/badge-72.png', data: payload.data }; self.registration.showNotification(notificationTitle, notificationOptions); }); // Handle notification clicks self.addEventListener('notificationclick', (event) => { event.notification.close(); const urlToOpen = event.notification.data?.url ?? '/'; event.waitUntil( clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) => { // Check if window is already open for (const client of clientList) { if (client.url === urlToOpen && 'focus' in client) { return client.focus(); } } // Open new window if (clients.openWindow) { return clients.openWindow(urlToOpen); } }) ); });

API Routes

Subscribe to Notifications

// app/api/notifications/subscribe/route.ts import { auth } from '@orbitusdev/auth'; import { prisma } from '@orbitusdev/database'; export async function POST(req: Request) { const session = await auth(); if (!session?.user) { return Response.json({ error: 'Unauthorized' }, { status: 401 }); } const { token } = await req.json(); // Save FCM token to database await prisma.notificationToken.upsert({ where: { userId: session.user.id }, update: { token }, create: { userId: session.user.id, token } }); return Response.json({ success: true }); }

Send Notification

// app/api/notifications/send/route.ts import { getPushNotificationService } from '@orbitusdev/core/services/push'; import { prisma } from '@orbitusdev/database'; export async function POST(req: Request) { const { userId, title, body, url } = await req.json(); // Get user's FCM token const tokenData = await prisma.notificationToken.findUnique({ where: { userId } }); if (!tokenData?.token) { return Response.json({ error: 'No token found' }, { status: 404 }); } // Send notification const pushService = getPushNotificationService(); await pushService.sendPushNotification({ token: tokenData.token, title, body, clickAction: url, data: { url } }); // Save to database await prisma.notification.create({ data: { userId, title, body, url, read: false } }); return Response.json({ success: true }); }

Use Cases

Order Updates

import { getPushNotificationService } from '@orbitusdev/core/services/push'; export async function notifyOrderStatus(order: { userId: string; orderId: string; status: string; }) { const token = await getUserFCMToken(order.userId); if (!token) return; const pushService = getPushNotificationService(); await pushService.sendPushNotification({ token, title: 'Order Update', body: `Your order #${order.orderId} is now ${order.status}`, clickAction: `/orders/${order.orderId}`, imageUrl: 'https://orbitus.dev/order-icon.png' }); }

New Message Notification

import { getPushNotificationService } from '@orbitusdev/core/services/push'; export async function notifyNewMessage(message: { recipientId: string; senderName: string; content: string; conversationId: string; }) { const token = await getUserFCMToken(message.recipientId); if (!token) return; const pushService = getPushNotificationService(); await pushService.sendPushNotification({ token, title: `New message from ${message.senderName}`, body: message.content.slice(0, 100), clickAction: `/messages/${message.conversationId}` }); }

Bulk Notifications

import { getPushNotificationService } from '@orbitusdev/core/services/push'; export async function sendToAllUsers(notification: { title: string; body: string; url?: string; }) { // Get all active tokens const tokens = await prisma.notificationToken.findMany({ where: { active: true }, select: { token: true } }); const tokenStrings = tokens.map((t) => t.token); const pushService = getPushNotificationService(); // Send in batches of 500 (FCM limit) const batchSize = 500; for (let i = 0; i < tokenStrings.length; i += batchSize) { const batch = tokenStrings.slice(i, i + batchSize); await pushService.sendMulticastNotification(batch, notification); } }

User Preferences

Notification Settings Schema

model NotificationPreferences { id String @id @default(cuid()) userId String @unique user User @relation(fields: [userId], references: [id]) orders Boolean @default(true) messages Boolean @default(true) marketing Boolean @default(false) updates Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }

Check Preferences Before Sending

import { getPushNotificationService } from '@orbitusdev/core/services/push'; export async function sendNotificationWithPreferences(params: { userId: string; type: 'orders' | 'messages' | 'marketing' | 'updates'; title: string; body: string; url?: string; }) { // Check user preferences const prefs = await prisma.notificationPreferences.findUnique({ where: { userId: params.userId } }); if (!prefs?.[params.type]) { return; // User disabled this notification type } const token = await getUserFCMToken(params.userId); if (!token) return; const pushService = getPushNotificationService(); await pushService.sendPushNotification({ token, title: params.title, body: params.body, clickAction: params.url }); }

Scheduled Notifications

// Using a cron job or scheduler import { getPushNotificationService } from '@orbitusdev/core/services/push'; export async function sendScheduledReminders() { const reminders = await prisma.reminder.findMany({ where: { scheduledFor: { lte: new Date(), gt: new Date(Date.now() - 60000) // Last minute }, sent: false }, include: { user: { include: { notificationToken: true } } } }); const pushService = getPushNotificationService(); for (const reminder of reminders) { if (!reminder.user.notificationToken?.token) continue; await pushService.sendPushNotification({ token: reminder.user.notificationToken.token, title: 'Reminder', body: reminder.message, clickAction: reminder.url }); await prisma.reminder.update({ where: { id: reminder.id }, data: { sent: true } }); } }

Best Practices

  1. Request permission at right time - Don’t ask immediately
  2. Provide value - Explain why notifications are useful
  3. Respect user preferences - Allow granular control
  4. Handle token refresh - FCM tokens can expire
  5. Test on multiple browsers - Safari, Chrome, Firefox
  6. Keep messages short - Title: 40 chars, body: 120 chars
  7. Use rich media - Images increase engagement
  8. Time notifications wisely - Respect time zones
  9. Track deliverability - Monitor success rates
  10. Provide opt-out - Easy unsubscribe mechanism

Error Handling

import { getPushNotificationService } from '@orbitusdev/core/services/push'; import { logger } from '@orbitusdev/core/services/logger'; export async function sendPushSafe(params: SendPushParams) { const pushService = getPushNotificationService(); try { const result = await pushService.sendPushNotification(params); logger.info('Push notification sent', { token: params.token.slice(0, 10) }); return result; } catch (error) { // Handle invalid token if (error.code === 'messaging/registration-token-not-registered') { await prisma.notificationToken.delete({ where: { token: params.token } }); logger.warn('Removed invalid FCM token', { token: params.token.slice(0, 10) }); } else { logger.error('Failed to send push notification', error); } throw error; } }

Testing

Test Notification

import { getPushNotificationService } from '@orbitusdev/core/services/push'; const testToken = 'YOUR_TEST_TOKEN'; const pushService = getPushNotificationService(); await pushService.sendPushNotification({ token: testToken, title: 'Test Notification', body: 'This is a test notification', clickAction: 'https://orbitus.dev' });

Mock Service

// tests/mocks/push.ts export const mockSendPush = jest.fn().mockResolvedValue({ success: true, messageId: 'projects/test/messages/12345' }); jest.mock('@orbitusdev/core/services/push', () => ({ sendPushNotification: mockSendPush }));

Last updated on