When an LLM proxy sits on the critical path — between your application and every model provider — observability is not optional. You need to know what's happening right now, not what happened 5 minutes ago after a batch ETL job caught up.
Why not WebSockets?
WebSockets are bidirectional. An audit log is unidirectional: the server emits events, the client consumes them. Bidirectional communication means:
More complex connection state management
Harder to reason about reconnection semantics
Authentication is trickier (WebSocket upgrade doesn't carry custom headers cleanly)
SSE (Server-Sent Events) is simpler: it's a long-lived HTTP response with `Content-Type: text/event-stream`. The browser's EventSource API handles reconnection natively with exponential backoff.
Tamga's SSE architecture
**Event Bus (in-process).** Every scanner finding, every policy change, every API key rotation publishes to an in-process event bus. Subscribers include the audit store (PostgreSQL) and the SSE fan-out.
**SSE Hub.** A goroutine-per-client model would not scale. Instead, a single hub goroutine fans out events to all connected dashboard clients via Go channels. A client that falls behind (slow network) gets its channel dropped and must reconnect.
**Client-side reconnect.** The dashboard's `useLiveEventsStream` hook wraps EventSource with:
`kind`: namespace-prefixed type (`policy.create`, `incident.create`, `request.scanned`, `request.blocked`)
`timestamp`: RFC 3339 with nanosecond precision
`payload`: JSON blob specific to the event kind
This schema is deliberately flat and filterable — the dashboard lets you filter by kind, time range, and severity without any backend query.
Lessons learned
**SSE works through most proxies** but some corporate proxies buffer responses. We added a `X-Accel-Buffering: no` header for nginx users.
**Go's `http.ResponseController`** (Go 1.20+) lets you flush the response writer without hijacking — use it.
**Client-side buffer management.** If the dashboard tab is backgrounded for 10 minutes and comes back, don't replay all 10 minutes of events. Drop and show a "live" indicator.
The full implementation is open source. See `proxy/internal/api/sse.go` and `dashboard/app/dashboard/events/useLiveEventsStream.ts`.