Server-Sent Events (SSE)
SseManager maintains a registry of open HTTP streams and broadcasts StreamEvent objects to all clients subscribed to a matching topic. The server controls which topics a connection may receive — clients cannot self-declare channels.
Connection lifecycle
Setup
Enable SSE when creating AuthTools:
const tools = new AuthTools(bus, { sse: true });
Mount the tools router (exposes GET /tools/stream):
app.use('/tools', createToolsRouter(tools, {
authMiddleware: auth.middleware(),
}));
Connecting from a browser
const evtSource = new EventSource('/tools/stream', { withCredentials: true });
evtSource.onmessage = (event) => {
const payload = JSON.parse(event.data);
console.log(payload.type, payload.data);
};
evtSource.addEventListener('security-alert', (event) => {
const payload = JSON.parse(event.data);
alert(`New login from ${payload.data.ip}`);
});
evtSource.onerror = () => evtSource.close();
Topic hierarchy
The server enforces topic access — restrict which topics each user/connection may subscribe to via authMiddleware.
| Topic | Description |
|---|---|
global | All connected clients |
tenant:{tenantId} | All clients in a tenant |
tenant:{tenantId}:role:{role} | All clients with a role in a tenant |
tenant:{tenantId}:group:{groupId} | All clients in a group |
user:{userId} | A specific user |
session:{sessionId} | A specific session |
custom:{namespace} | Application-defined namespace |
StreamEvent structure
interface StreamEvent<T = unknown> {
id: string; // auto-generated UUID
type: string; // event type name
timestamp: string; // ISO 8601
topic: string; // channel this was published to
data: T; // arbitrary payload
userId?: string;
tenantId?: string;
metadata?: Record<string, unknown>;
}
SseManager API
class SseManager {
readonly heartbeatIntervalMs: number; // default: 30000
addConnection(
res: Response,
topics: string[],
meta?: { userId?: string; tenantId?: string },
): string; // returns connectionId
removeConnection(connectionId: string): void;
broadcast(topic: string, event: Omit<StreamEvent, 'id' | 'timestamp' | 'topic'>): void;
broadcastToUser(userId: string, event: Omit<StreamEvent, 'id' | 'timestamp' | 'topic'>): void;
broadcastToTenant(tenantId: string, event: Omit<StreamEvent, 'id' | 'timestamp' | 'topic'>): void;
get connectionCount(): number;
}