Notifications, progress bars, live feeds — these don't need WebSocket's bidirectional complexity. Server-Sent Events give you server-to-client streaming with automatic reconnection built in. Claude Code generates the full implementation.
CLAUDE.md for SSE Standards
## Server-Sent Events (SSE) Rules
### When SSE is the right choice
- Server → client one-way streaming
- Notifications, progress updates, live feeds, dashboard updates
- No client-to-server data needed
### Use WebSocket instead when
- Bidirectional communication (chat, games, collaboration)
- Binary data transfer
### Implementation rules
- Headers: Content-Type: text/event-stream, Cache-Control: no-cache
- X-Accel-Buffering: no (disable Nginx buffering)
- Auth: query parameter token (EventSource can't set Authorization header)
- Reconnection: use Last-Event-ID to resume from where client left off
- Heartbeat: send comment every 30 seconds to keep connection alive
- Max connections: 5 per user
### Security
- SSE endpoints require authentication (same as any API)
- Set CORS headers appropriately
- Validate token in query parameter before sending any events
Generating the SSE Server
Generate an SSE endpoint with Express + TypeScript.
Requirements:
- Endpoint: GET /api/events
- JWT auth via query parameter: ?token=xxx
- Event types: notification, update, heartbeat
- Heartbeat: every 30 seconds
- Connection management: Map by userId
- Auto-cleanup on disconnect
Generate:
- src/sse/sseManager.ts (connection manager)
- src/routes/events.ts (SSE endpoint)
Generated SSE Manager
// src/sse/sseManager.ts
import { Response } from 'express';
interface SSEClient {
userId: string;
res: Response;
lastEventId?: string;
}
interface SSEEvent {
type: string;
data: unknown;
id?: string;
}
class SSEManager {
private clients = new Map<string, Set<SSEClient>>();
constructor() {
// Heartbeat every 30 seconds to keep connections alive
setInterval(() => {
this.broadcastComment('heartbeat');
}, 30_000);
}
addClient(userId: string, client: SSEClient): boolean {
if (!this.clients.has(userId)) {
this.clients.set(userId, new Set());
}
const userClients = this.clients.get(userId)!;
if (userClients.size >= 5) {
return false; // Max connections exceeded
}
userClients.add(client);
client.res.on('close', () => {
userClients.delete(client);
if (userClients.size === 0) {
this.clients.delete(userId);
}
});
return true;
}
sendToUser(userId: string, event: SSEEvent): void {
const userClients = this.clients.get(userId);
if (!userClients) return;
for (const client of userClients) {
this.writeEvent(client.res, event);
}
}
broadcast(event: SSEEvent): void {
for (const clients of this.clients.values()) {
for (const client of clients) {
this.writeEvent(client.res, event);
}
}
}
private broadcastComment(comment: string): void {
for (const clients of this.clients.values()) {
for (const client of clients) {
client.res.write(`: ${comment}
`);
}
}
}
private writeEvent(res: Response, event: SSEEvent): void {
if (event.id) res.write(`id: ${event.id}
`);
res.write(`event: ${event.type}
`);
res.write(`data: ${JSON.stringify(event.data)}
`);
}
}
export const sseManager = new SSEManager();
// src/routes/events.ts
eventsRouter.get('/events', async (req, res) => {
// Query param token — EventSource can't set Authorization header
const token = req.query.token as string;
const user = await verifyToken(token);
if (!user) return res.status(401).end();
// SSE headers
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no'); // Nginx: disable buffering
res.flushHeaders();
const added = sseManager.addClient(user.id, {
userId: user.id,
res,
lastEventId: req.headers['last-event-id'] as string,
});
if (!added) {
res.write('event: error
data: {"message":"max connections exceeded"}
');
return res.end();
}
// Confirm connection
res.write(`event: connected
data: {"userId":"${user.id}"}
`);
});
Sending Events from Services
// From any service — send notification to specific user
import { sseManager } from '../sse/sseManager';
// When order is placed
async function onOrderPlaced(order: Order) {
sseManager.sendToUser(order.userId, {
type: 'notification',
data: {
title: 'Order placed',
message: `Order #${order.id} confirmed`,
orderId: order.id,
},
id: order.id, // For Last-Event-ID reconnection
});
}
React Client Hook
Generate a React hook for SSE connection.
Requirements:
- Custom hook: useSSE(url, options)
- Per-event-type callbacks
- Auto-reconnect using EventSource native behavior (Last-Event-ID)
- Show error state in UI on connection failure
- EventSource.close() on component unmount
// src/hooks/useSSE.ts
import { useEffect, useRef } from 'react';
interface SSEOptions {
onNotification?: (data: unknown) => void;
onUpdate?: (data: unknown) => void;
onError?: (error: Event) => void;
}
export function useSSE(url: string, options: SSEOptions) {
const eventSourceRef = useRef<EventSource | null>(null);
useEffect(() => {
const es = new EventSource(url);
es.addEventListener('notification', (e: MessageEvent) => {
options.onNotification?.(JSON.parse(e.data));
});
es.addEventListener('update', (e: MessageEvent) => {
options.onUpdate?.(JSON.parse(e.data));
});
es.onerror = (e) => {
options.onError?.(e);
// EventSource reconnects automatically with Last-Event-ID
};
eventSourceRef.current = es;
return () => {
es.close();
eventSourceRef.current = null;
};
}, [url]);
}
Summary
Implement SSE with Claude Code:
- CLAUDE.md — Define use cases, header requirements, auth approach
- SSEManager — Map-based connection management per userId
- Heartbeat — 30-second comments to keep connections alive
- Client hook — Auto-reconnect with Last-Event-ID built into EventSource
Security Pack (¥1,480) includes /security-check for real-time endpoint security review — auth gaps, missing CORS, max connection limits.
Myouga (@myougatheaxo) — Claude Code engineer focused on real-time web patterns.
Top comments (0)