Audience: Frontend + Backend Engineers
Outcomes: Reliable real-time UX; versionable event contracts
Server (Node)
const clients = new Set<import('http').ServerResponse>(); app.get('/api/events/stream', (req, res) => { res.set({ 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' }); res.flushHeaders(); clients.add(res); req.on('close', () => clients.delete(res)); }); function emitEvent(name: string, data: any) { const payload = `event: ${name}\ndata: ${JSON.stringify(data)}\n\n`; for (const res of clients) if (!res.destroyed) res.write(payload); }
Client (browser)
const es = new EventSource('/api/events/stream', { withCredentials: true }); ['order.status.changed','payment.succeeded','dispute.opened'].forEach(name => { es.addEventListener(name, e => console.log(name, JSON.parse(e.data))); });
Event catalog (selection)
Event | Payload (short) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Enrichment & filtering
Subscribe only to needed names; stream is org-scoped
Enrich server-side with tiny derived fields (e.g.,
displayAmount
)Anti-pattern: dumping whole objects on every event
Change policy
Additive changes (new fields) are safe
Breaking changes → new event name or versioned channel
QA checklist
CORS allows your origin; proxies don’t buffer SSE
Auto-reconnect works; handlers are idempotent
Runbook: “SSE outage”
Disable proxy buffering; check TLS/CORS; confirm
withCredentials:true
.