Learn the concept
Real-Time System Architecture
A real-time notification system uses Server-Sent Events (SSE) or WebSockets for delivery, a priority queue for ordering, toast UI for immediate alerts, a notification center for history, and the Notifications API for browser-level alerts when the tab is not focused.
Real-time notifications are a critical feature in modern web applications. Designing one well requires decisions about transport mechanism, state management, UI patterns, offline handling, and browser permissions.
Functional Requirements:
Non-Functional Requirements:
| Feature | SSE | WebSocket | Long Polling | |---------|-----|-----------|-------------| | Direction | Server → Client only | Bidirectional | Server → Client | | Protocol | HTTP/2 | WS (separate) | HTTP | | Auto-reconnect | Built-in | Manual | Manual | | Complexity | Low | Medium | Low | | Best for | Notifications, feeds | Chat, gaming | Legacy fallback |
SSE (Server-Sent Events) is the best choice for notifications because:
Last-Event-ID for resuming missed eventsEventSourceUse WebSockets only if you also need client-to-server real-time communication (e.g., chat, collaborative editing).
SSE handles reconnection automatically, but you should enhance it:
Last-Event-ID header on reconnect so the server can replay missed eventsNot all notifications are equal. Implement a priority system:
Grouping reduces notification fatigue:
Notification state includes:
{
notifications: Notification[] // All notifications
unreadCount: number // Badge count
toastQueue: Notification[] // Active toasts
connectionStatus: 'connected' | 'reconnecting' | 'disconnected'
}
Key decisions:
When the user goes offline:
navigator.onLine and the offline/online eventsLast-Event-ID replays missed eventsGET /api/notifications?since={lastEventId}For when the user's tab is not focused:
Notification.requestPermission()document.visibilityState === 'hidden' to decide whether to show browser notificationtype NotificationPriority = 'urgent' | 'high' | 'medium' | 'low';
interface AppNotification {
id: string;
type: string;
title: string;
body: string;
priority: NotificationPriority;
timestamp: number;
read: boolean;
groupKey?: string;
}
type ConnectionStatus = 'connected' | 'reconnecting' | 'disconnected';
type StatusCallback = (status: ConnectionStatus) => void;
type NotificationCallback = (notification: AppNotification) => void;
export function createNotificationStream(
onNotification: NotificationCallback,
onStatusChange: StatusCallback
) {
let eventSource: EventSource | null = null;
let lastEventId: string | null = null;
function connect() {
// Build URL with last event ID for replay
const url = lastEventId
? `/api/notifications/stream?lastEventId=${lastEventId}`
: '/api/notifications/stream';
eventSource = new EventSource(url);
eventSource.onopen = () => {
onStatusChange('connected');
};
// Listen for notification events
eventSource.addEventListener('notification', (event) => {
lastEventId = event.lastEventId;
const notification: AppNotification = JSON.parse(event.data);
onNotification(notification);
// Show browser notification if tab is hidden
if (
document.visibilityState === 'hidden' &&
(notification.priority === 'urgent' ||
notification.priority === 'high')
) {
showBrowserNotification(notification);
}
});
// SSE auto-reconnects on error
eventSource.onerror = () => {
onStatusChange('reconnecting');
// EventSource handles reconnection automatically
// with exponential backoff
};
}
function showBrowserNotification(notification: AppNotification) {
if (Notification.permission === 'granted') {
const n = new Notification(notification.title, {
body: notification.body,
tag: notification.id, // Prevents duplicates
requireInteraction: notification.priority === 'urgent',
});
n.onclick = () => {
window.focus();
n.close();
};
}
}
function disconnect() {
eventSource?.close();
eventSource = null;
onStatusChange('disconnected');
}
function requestBrowserPermission(): Promise<NotificationPermission> {
return Notification.requestPermission();
}
// Start connection
connect();
// Reconnect when coming back online
window.addEventListener('online', () => {
if (!eventSource || eventSource.readyState === EventSource.CLOSED) {
connect();
}
});
return { disconnect, requestBrowserPermission };
}Facebook, Twitter/X, and LinkedIn use real-time notifications for likes, comments, shares, and follows. They group notifications (e.g., '5 people liked your post') and prioritize direct interactions over passive updates.
Amazon and Shopify send real-time order status updates (confirmed, shipped, delivered) via in-app notifications and browser push, keeping customers informed without requiring them to check their email.
Grafana and PagerDuty use real-time notification systems to alert on-call engineers about incidents with priority-based routing and escalation policies.
Build a notification center component with a bell icon showing unread count, a dropdown with notification history, and a toast stack for new alerts. Include mark-as-read, mark-all-read, and auto-dismiss functionality.
Create a collaborative activity feed that shows real-time updates from multiple users using SSE. Include reconnection handling, offline indicators, and event replay on reconnect.
Uses WebSockets for its real-time messaging and notification system, with sophisticated presence detection and do-not-disturb scheduling to prevent notification fatigue
Uses SSE for real-time notification delivery of PR reviews, issue mentions, and CI/CD status updates, with a notification center that supports filtering by repository and type
Combines WebSockets for real-time collaboration with notification events for comments and file updates, using multiplayer cursors and presence indicators