Skip to main content

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.

TopicDescription
globalAll 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;
}