PatternsReal-Time Architecture
Live Notifications
Build real-time notification systems with push, badges, and permission handling
Real-time notifications keep users engaged. This guide covers in-app, push, and browser notifications.
đ¯ Types of Notifications
In-App: Toast, banner, badge (user is in app)
Push: Browser notification (user may be away)
Email/SMS: Fallback (user is offline)đ Pattern 1: In-App Notifications
Real-time notifications within the app.
interface Notification {
id: string;
type: 'info' | 'success' | 'warning' | 'error';
title: string;
message: string;
timestamp: Date;
read: boolean;
actionUrl?: string;
icon?: string;
}
class NotificationManager {
private ws: WebSocket;
private notifications: Notification[] = [];
private listeners = new Set<(notifications: Notification[]) => void>();
constructor(ws: WebSocket) {
this.ws = ws;
this.setupWebSocket();
this.loadFromStorage();
}
private setupWebSocket() {
this.ws.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.type === 'notification') {
this.addNotification(message.notification);
}
};
}
private addNotification(notification: Notification) {
this.notifications.unshift(notification);
// Keep max 100 notifications
if (this.notifications.length > 100) {
this.notifications = this.notifications.slice(0, 100);
}
this.saveToStorage();
this.notifyListeners();
// Show toast
this.showToast(notification);
}
private showToast(notification: Notification) {
// Use your preferred toast library
toast(notification.message, {
icon: notification.type === 'success' ? 'â
' :
notification.type === 'error' ? 'â' :
notification.type === 'warning' ? 'â ī¸' : 'âšī¸',
duration: 5000,
onClick: notification.actionUrl ?
() => window.location.href = notification.actionUrl! :
undefined
});
}
markAsRead(id: string) {
const notification = this.notifications.find(n => n.id === id);
if (notification && !notification.read) {
notification.read = true;
this.saveToStorage();
this.notifyListeners();
// Notify server
this.ws.send(JSON.stringify({
type: 'notification_read',
notificationId: id
}));
}
}
markAllAsRead() {
this.notifications.forEach(n => n.read = true);
this.saveToStorage();
this.notifyListeners();
this.ws.send(JSON.stringify({
type: 'notifications_read_all'
}));
}
getUnreadCount(): number {
return this.notifications.filter(n => !n.read).length;
}
getNotifications(): Notification[] {
return this.notifications;
}
private notifyListeners() {
this.listeners.forEach(listener => listener(this.notifications));
}
subscribe(listener: (notifications: Notification[]) => void): () => void {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
private saveToStorage() {
localStorage.setItem('notifications', JSON.stringify(this.notifications));
}
private loadFromStorage() {
const stored = localStorage.getItem('notifications');
if (stored) {
this.notifications = JSON.parse(stored);
}
}
}React Components
// Notification Bell
function NotificationBell() {
const [notifications, setNotifications] = useState<Notification[]>([]);
const [isOpen, setIsOpen] = useState(false);
const manager = useRef<NotificationManager>();
useEffect(() => {
manager.current = new NotificationManager(ws);
const unsubscribe = manager.current.subscribe(setNotifications);
return unsubscribe;
}, []);
const unreadCount = notifications.filter(n => !n.read).length;
return (
<div className="notification-bell">
<button onClick={() => setIsOpen(!isOpen)}>
đ
{unreadCount > 0 && (
<span className="badge">{unreadCount}</span>
)}
</button>
{isOpen && (
<NotificationDropdown
notifications={notifications}
onMarkAsRead={(id) => manager.current?.markAsRead(id)}
onMarkAllAsRead={() => manager.current?.markAllAsRead()}
/>
)}
</div>
);
}
// Notification Dropdown
function NotificationDropdown({
notifications,
onMarkAsRead,
onMarkAllAsRead
}: {
notifications: Notification[];
onMarkAsRead: (id: string) => void;
onMarkAllAsRead: () => void;
}) {
return (
<div className="notification-dropdown">
<div className="header">
<h3>Notifications</h3>
{notifications.some(n => !n.read) && (
<button onClick={onMarkAllAsRead}>Mark all as read</button>
)}
</div>
<div className="list">
{notifications.length === 0 ? (
<div className="empty">No notifications</div>
) : (
notifications.map(notification => (
<NotificationItem
key={notification.id}
notification={notification}
onMarkAsRead={onMarkAsRead}
/>
))
)}
</div>
</div>
);
}
// Notification Item
function NotificationItem({
notification,
onMarkAsRead
}: {
notification: Notification;
onMarkAsRead: (id: string) => void;
}) {
return (
<div
className={`notification-item ${notification.read ? 'read' : 'unread'}`}
onClick={() => {
onMarkAsRead(notification.id);
if (notification.actionUrl) {
window.location.href = notification.actionUrl;
}
}}
>
{notification.icon && <span className="icon">{notification.icon}</span>}
<div className="content">
<h4>{notification.title}</h4>
<p>{notification.message}</p>
<span className="time">{formatTimeAgo(notification.timestamp)}</span>
</div>
{!notification.read && <span className="unread-dot" />}
</div>
);
}đ¨ Pattern 2: Push Notifications (Browser)
Send notifications even when app is closed.
class PushNotificationManager {
async requestPermission(): Promise<NotificationPermission> {
if (!('Notification' in window)) {
console.error('This browser does not support notifications');
return 'denied';
}
const permission = await Notification.requestPermission();
console.log('Notification permission:', permission);
if (permission === 'granted') {
await this.subscribeToPush();
}
return permission;
}
async subscribeToPush() {
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
console.error('Push notifications not supported');
return;
}
try {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: this.urlBase64ToUint8Array(
process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!
)
});
// Send subscription to server
await fetch('/api/push/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(subscription)
});
console.log('Push subscription successful');
} catch (error) {
console.error('Failed to subscribe to push:', error);
}
}
async unsubscribe() {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.getSubscription();
if (subscription) {
await subscription.unsubscribe();
// Notify server
await fetch('/api/push/unsubscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ endpoint: subscription.endpoint })
});
}
}
async showNotification(title: string, options: NotificationOptions) {
if (Notification.permission !== 'granted') {
console.warn('Notification permission not granted');
return;
}
const registration = await navigator.serviceWorker.ready;
await registration.showNotification(title, {
...options,
badge: '/badge.png',
icon: '/icon.png',
vibrate: [200, 100, 200],
tag: options.tag || 'default',
renotify: true
});
}
private urlBase64ToUint8Array(base64String: string): Uint8Array {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
}
// React Component
function PushNotificationSetup() {
const [permission, setPermission] = useState<NotificationPermission>('default');
const manager = useRef(new PushNotificationManager());
useEffect(() => {
if ('Notification' in window) {
setPermission(Notification.permission);
}
}, []);
const handleEnable = async () => {
const result = await manager.current.requestPermission();
setPermission(result);
};
if (permission === 'granted') {
return <div>â
Push notifications enabled</div>;
}
if (permission === 'denied') {
return (
<div>
â Push notifications blocked. Please enable in browser settings.
</div>
);
}
return (
<button onClick={handleEnable}>
Enable Push Notifications
</button>
);
}Service Worker (public/sw.js)
self.addEventListener('push', (event) => {
const data = event.data.json();
const options = {
body: data.message,
icon: data.icon || '/icon.png',
badge: '/badge.png',
data: {
url: data.url
},
actions: data.actions || []
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
if (event.notification.data?.url) {
event.waitUntil(
clients.openWindow(event.notification.data.url)
);
}
});đĸ Pattern 3: Badge Count
Show unread count on icon/favicon.
class BadgeManager {
private count = 0;
setCount(count: number) {
this.count = count;
this.updateFavicon();
this.updateTitle();
// Update app badge (PWA)
if ('setAppBadge' in navigator) {
if (count > 0) {
(navigator as any).setAppBadge(count);
} else {
(navigator as any).clearAppBadge();
}
}
}
private updateFavicon() {
const canvas = document.createElement('canvas');
canvas.width = 32;
canvas.height = 32;
const ctx = canvas.getContext('2d');
if (!ctx) return;
// Draw base favicon
const favicon = new Image();
favicon.src = '/favicon.ico';
favicon.onload = () => {
ctx.drawImage(favicon, 0, 0, 32, 32);
if (this.count > 0) {
// Draw badge
ctx.fillStyle = '#ff0000';
ctx.beginPath();
ctx.arc(24, 8, 8, 0, 2 * Math.PI);
ctx.fill();
// Draw count
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 12px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(
this.count > 9 ? '9+' : this.count.toString(),
24,
8
);
}
// Update favicon
const link = document.querySelector('link[rel="icon"]') as HTMLLinkElement;
if (link) {
link.href = canvas.toDataURL();
}
};
}
private updateTitle() {
const baseTitle = 'My App';
if (this.count > 0) {
document.title = `(${this.count}) ${baseTitle}`;
} else {
document.title = baseTitle;
}
}
}
// React Hook
function useBadgeCount(notifications: Notification[]) {
const badgeManager = useRef(new BadgeManager());
useEffect(() => {
const unreadCount = notifications.filter(n => !n.read).length;
badgeManager.current.setCount(unreadCount);
}, [notifications]);
}đ¯ Pattern 4: Notification Preferences
Let users control notifications.
interface NotificationPreferences {
enabled: boolean;
types: {
comments: boolean;
likes: boolean;
mentions: boolean;
messages: boolean;
};
quietHours: {
enabled: boolean;
start: string; // "22:00"
end: string; // "08:00"
};
push: boolean;
email: boolean;
}
function NotificationSettings() {
const [prefs, setPrefs] = useState<NotificationPreferences>({
enabled: true,
types: {
comments: true,
likes: true,
mentions: true,
messages: true
},
quietHours: {
enabled: false,
start: '22:00',
end: '08:00'
},
push: false,
email: true
});
const updatePreferences = async (newPrefs: NotificationPreferences) => {
setPrefs(newPrefs);
await fetch('/api/settings/notifications', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newPrefs)
});
};
return (
<div className="notification-settings">
<h2>Notification Settings</h2>
<div>
<label>
<input
type="checkbox"
checked={prefs.enabled}
onChange={(e) => updatePreferences({
...prefs,
enabled: e.target.checked
})}
/>
Enable notifications
</label>
</div>
<h3>Notification Types</h3>
{Object.entries(prefs.types).map(([type, enabled]) => (
<label key={type}>
<input
type="checkbox"
checked={enabled}
onChange={(e) => updatePreferences({
...prefs,
types: { ...prefs.types, [type]: e.target.checked }
})}
/>
{type.charAt(0).toUpperCase() + type.slice(1)}
</label>
))}
<h3>Quiet Hours</h3>
<label>
<input
type="checkbox"
checked={prefs.quietHours.enabled}
onChange={(e) => updatePreferences({
...prefs,
quietHours: { ...prefs.quietHours, enabled: e.target.checked }
})}
/>
Enable quiet hours
</label>
{prefs.quietHours.enabled && (
<div>
<input
type="time"
value={prefs.quietHours.start}
onChange={(e) => updatePreferences({
...prefs,
quietHours: { ...prefs.quietHours, start: e.target.value }
})}
/>
<span>to</span>
<input
type="time"
value={prefs.quietHours.end}
onChange={(e) => updatePreferences({
...prefs,
quietHours: { ...prefs.quietHours, end: e.target.value }
})}
/>
</div>
)}
</div>
);
}đ Key Takeaways
- In-app first - Always show in-app notifications
- Request permission wisely - Explain why before asking
- Respect preferences - Let users control everything
- Badge count - Show unread on icon
- Quiet hours - Don't disturb at night
- Group notifications - "3 new messages" not 3 separate
- Test on mobile - Push works differently
Notifications are powerful but easily annoying. Use them wisely!