Notifications & Real-Time (SSE)
DBS includes a full-featured real-time notification system that combines Server-Sent Events (SSE) for live push with SWR polling as a fallback. All state is managed through a global React Context.
Architecture
Browser (Client)
βββ EventSource('/api/stream/events') β persistent SSE connection
βββ SWR polling /api/notifications β 30s fallback
Server (Next.js)
βββ /api/stream/events β SSE endpoint
βββ eventBus (src/lib/events.ts) β in-process EventEmitter
βββ fires 'system-event' when:
βββ task-completed (background task done)
βββ broadcast (admin broadcast message)Key Files
| File | Purpose |
|---|---|
src/lib/notification-package/NotificationContext.tsx | React Context + SSE connection lifecycle |
src/lib/notification-package/useNotifications.ts | SWR hook for notifications CRUD |
src/lib/notification-package/useTasks.ts | SWR hook for background task status |
src/lib/events.ts | In-process Node.js EventEmitter (the event bus) |
src/app/api/stream/events/route.ts | SSE HTTP endpoint |
src/app/api/notifications/route.ts | REST API for notifications CRUD |
Using the Notification System
Access Global State
Use useNotificationSystem() anywhere inside the dashboard:
'use client';
import { useNotificationSystem } from '@/lib/notification-package';
export function NotificationBell() {
const {
notifications, // Notification[]
tasks, // Task[]
unreadCount, // number
markAsRead, // (id: string) => Promise<void>
markAllAsRead, // () => Promise<void>
refresh, // () => void β force re-fetch
} = useNotificationSystem();
return (
<button>
<Bell />
{unreadCount > 0 && <Badge>{unreadCount}</Badge>}
</button>
);
}useNotificationSystem() must be called inside a component wrapped by NotificationProvider. In DBS, the provider is already mounted in src/app/dashboard/layout.tsx, so all dashboard pages have access automatically.
Emitting a Real-Time Event from the Server
Push a live event to connected clients from any API route using eventBus:
Task Completed
import { eventBus } from '@/lib/events';
// After a background task finishes
eventBus.emit('system-event', {
type: 'task-completed',
userId: task.userId, // Only this user receives the event
taskId: task.id,
status: 'completed',
});SSE Endpoint Details
// src/app/api/stream/events/route.ts
export async function GET(req: Request) {
const guard = await apiGuard();
if (guard.error) return guard.error;
const { session } = guard;
const stream = new ReadableStream({
start(controller) {
const encoder = new TextEncoder();
// Send heartbeat every 30 seconds
const heartbeat = setInterval(() => {
controller.enqueue(encoder.encode(': heartbeat\n\n'));
}, 30_000);
// Listen for events
const handler = (data: SystemEvent) => {
// Only send to the correct user (or broadcast to all)
if (!data.userId || data.userId === session.user.id) {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(data)}\n\n`)
);
}
};
eventBus.on('system-event', handler);
// Cleanup on disconnect
req.signal.addEventListener('abort', () => {
clearInterval(heartbeat);
eventBus.off('system-event', handler);
controller.close();
});
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
}| Property | Value |
|---|---|
| Endpoint | GET /api/stream/events |
| Auth Required | Yes β returns 401 if unauthenticated |
| Heartbeat Interval | 30 seconds (: heartbeat SSE comment) |
| Auto-reconnect | Browser EventSource retries after ~3s by default |
Notification REST API
| Method | Endpoint | Description |
|---|---|---|
GET | /api/notifications?page=1&limit=50 | Fetch paginated notifications |
PATCH | /api/notifications | Mark one ({ id }) or all ({ all: true }) as read |
DELETE | /api/notifications | Delete one ({ id }) or all ({ all: true }) |
POST | /api/notifications/broadcast | Create a notification for all users + emit SSE event |
Notification Data Model
interface Notification {
id: string;
userId: string;
title: string;
message: string;
type: 'info' | 'warning' | 'success' | 'error';
read: boolean;
createdAt: string;
}The SSE pattern works well in a single-instance deployment (Vercel, Docker). If you run multiple instances (Kubernetes, horizontal scaling), the in-process eventBus will only fire to clients connected to the same instance. For multi-instance deployments, replace eventBus with a pub/sub system like Redis Pub/Sub or Upstash Redis.