# How Server-Sent Events (SSE) Enable Real-Time UI Updates in AgentsView

> Learn how AgentsView uses Server-Sent Events to stream real-time UI updates from a Go backend directly to the browser, eliminating wasteful polling.

- Repository: [Kenn Software/agentsview](https://github.com/kenn-io/agentsview)
- Tags: how-to-guide
- Published: 2026-07-04

---

**AgentsView uses a long-running HTTP connection that streams Server-Sent Events to push database changes from the Go backend to the browser instantly, eliminating the need for polling.**

The `kenn-io/agentsview` repository implements a push-only real-time architecture using Server-Sent Events (SSE) to synchronize the SQLite database state with the single-page application (SPA) frontend. By maintaining an open HTTP stream, the server can broadcast incremental updates—such as new messages, sync progress, or insight generation—without client-side polling. This approach reduces latency and ensures the UI remains perfectly synchronized with the underlying data.

## SSE Architecture Overview

AgentsView’s SSE implementation centers on three core components: the stream initializer that establishes the HTTP connection, the event emitter that formats and transmits data, and the server-side wiring that monitors database changes and broadcasts them to active clients.

### Stream Initialization and Headers

In [`internal/server/sse.go`](https://github.com/kenn-io/agentsview/blob/main/internal/server/sse.go), the `NewSSEStream` function validates that the `http.ResponseWriter` implements `http.Flusher`, confirming the server supports streaming responses. It then writes the mandatory SSE headers required by the browser’s `EventSource` API:

- `Content-Type: text/event-stream`
- `Cache-Control: no-cache`
- `Connection: keep-alive`

After flushing the headers, the function returns an `SSEStream` struct that wraps the response writer. This setup establishes a persistent connection exempt from the server’s generic `WriteTimeout` (as noted in [`internal/server/timeout_test.go`](https://github.com/kenn-io/agentsview/blob/main/internal/server/timeout_test.go)), allowing the stream to remain open for minutes while awaiting new data.

### Event Emission Methods

The `SSEStream` type provides two primary methods for transmitting data:

- **`Send(event, data string)`**: Writes a plain-text SSE line following the format `event: …\ndata: …\n\n` and immediately flushes the writer. A `sseWriteTimeout` of 3 seconds prevents stuck clients from blocking the server indefinitely.
- **`SendJSON(event, v any)`**: Marshals a Go value to JSON and forwards it to `Send`. Both methods return `false` on error, allowing the handler to terminate the stream gracefully.

## Server-Side Event Wiring

The backend aggregates database changes and distributes them to all subscribed SSE streams using a monitor and broadcaster pattern.

### Session Monitoring

The `sessionMonitor` function in [`internal/server/events.go`](https://github.com/kenn-io/agentsview/blob/main/internal/server/events.go) creates a change-detection channel for a specific session ID. It instantiates a `Watcher` from [`internal/sessionwatch/watcher.go`](https://github.com/kenn-io/agentsview/blob/main/internal/sessionwatch/watcher.go) that polls the SQLite store and emits notifications whenever the session state changes:

```go
func (s *Server) sessionMonitor(ctx context.Context, sessionID string) <-chan struct{} {
    return sessionwatch.New(s.db, s.engine).Events(ctx, sessionID)
}

```

This channel blocks the SSE handler until a change occurs, ensuring events are only sent when the database actually updates.

### Broadcasting to Clients

In [`internal/server/server.go`](https://github.com/kenn-io/agentsview/blob/main/internal/server/server.go), the `WithBroadcaster` middleware aggregates all active SSE streams. When the database changes—triggered by new messages, completed syncs, or insight generation—the broadcaster invokes `Send` or `SendJSON` on each registered `SSEStream`.

The SSE endpoints (e.g., `/api/v1/watch`, `/api/v1/sync`) are registered in the server’s router via `Server.routes`. Each handler follows this pattern:

1. Initialize the stream with `NewSSEStream(w)`
2. Subscribe to the monitor channel for the requested session
3. Enter a `select` loop that blocks on context cancellation or the monitor channel
4. Emit events such as `"session_updated"`, `"messages"`, `"progress"`, or `"done"` until the client disconnects

## Client-Side Integration

The frontend consumes these streams using the standard JavaScript `EventSource` API. In [`frontend/src/lib/api/client.ts`](https://github.com/kenn-io/agentsview/blob/main/frontend/src/lib/api/client.ts), the SPA creates an `EventSource` pointing at `/api/v1/watch` and registers listeners for specific event names:

```javascript
const src = new EventSource("/api/v1/watch?session_id=123");
src.addEventListener("session_updated", ev => {
    const data = JSON.parse(ev.data);
    updateSessionUI(data);
});
src.addEventListener("error", () => src.close());

```

When a payload arrives, the listener immediately updates the UI state, providing a seamless live-refresh experience without polling overhead.

## Implementation Examples

### Server-Side Handler

This example from [`internal/server/sse.go`](https://github.com/kenn-io/agentsview/blob/main/internal/server/sse.go) demonstrates creating a stream and publishing JSON events:

```go
func (s *Server) handleWatch(w http.ResponseWriter, r *http.Request) {
    // Initialise the stream (sets headers, flushes initial line).
    stream, err := NewSSEStream(w)
    if err != nil {
        http.Error(w, "streaming not supported", http.StatusInternalServerError)
        return
    }
    // Subscribe to changes for a particular session.
    events := s.sessionMonitor(r.Context(), r.URL.Query().Get("session_id"))
    for {
        select {
        case <-r.Context().Done():
            // Client disconnected.
            return
        case <-events:
            // DB changed – push a JSON payload.
            payload := map[string]any{"type": "session_updated"}
            if !stream.SendJSON("session_updated", payload) {
                return // write error – stop streaming.
            }
        }
    }
}

```

### Client-Side Listener

The corresponding browser implementation parses incoming events and triggers UI updates:

```javascript
const src = new EventSource("/api/v1/watch?session_id=123");
src.addEventListener("session_updated", ev => {
  const data = JSON.parse(ev.data);
  // Update UI state with the new session information.
  updateSessionUI(data);
});
src.addEventListener("error", () => src.close());   // cleanup on failure.

```

## Summary

- **Long-running HTTP**: AgentsView uses `NewSSEStream` in [`internal/server/sse.go`](https://github.com/kenn-io/agentsview/blob/main/internal/server/sse.go) to establish persistent connections with proper SSE headers, bypassing standard write timeouts.
- **Structured events**: The `Send` and `SendJSON` methods format data according to the SSE specification and apply a 3-second write timeout to prevent resource exhaustion.
- **Database-driven**: `sessionMonitor` in [`internal/server/events.go`](https://github.com/kenn-io/agentsview/blob/main/internal/server/events.go) watches SQLite changes via [`internal/sessionwatch/watcher.go`](https://github.com/kenn-io/agentsview/blob/main/internal/sessionwatch/watcher.go) and signals the broadcaster.
- **Push architecture**: The `WithBroadcaster` mechanism in [`internal/server/server.go`](https://github.com/kenn-io/agentsview/blob/main/internal/server/server.go) distributes events to all active SSE streams, eliminating polling.
- **Browser consumption**: The frontend uses `EventSource` in [`frontend/src/lib/api/client.ts`](https://github.com/kenn-io/agentsview/blob/main/frontend/src/lib/api/client.ts) to receive real-time updates and refresh the UI immediately.

## Frequently Asked Questions

### What is the difference between SSE and WebSockets in AgentsView?

AgentsView uses Server-Sent Events because its architecture requires **server-to-client push only**, not bidirectional communication. SSE leverages standard HTTP, automatically handles reconnection via the browser’s `EventSource` API, and passes through most corporate firewalls and proxies without protocol upgrades. WebSockets would add unnecessary complexity for a one-way data flow from the SQLite database to the UI.

### How does AgentsView prevent SSE connections from blocking the server?

The `Send` method in [`internal/server/sse.go`](https://github.com/kenn-io/agentsview/blob/main/internal/server/sse.go) applies a `sseWriteTimeout` of 3 seconds to every write operation. If a client becomes unresponsive, the write fails quickly, returning `false` and allowing the handler to close the stream. Additionally, the broadcaster removes disconnected clients from the active stream registry, preventing memory leaks.

### Why does the SSE response bypass the server’s WriteTimeout?

According to comments in [`internal/server/timeout_test.go`](https://github.com/kenn-io/agentsview/blob/main/internal/server/timeout_test.go), the SSE response is explicitly exempt from the generic `WriteTimeout` because the connection must remain open for minutes to stream real-time updates. Without this exemption, the server would terminate long-running watch requests prematurely, breaking the live-update functionality.

### What types of events does AgentsView stream to the client?

The server emits several event types through the `/api/v1/watch` endpoint, including `"session_updated"` for state changes, `"messages"` for new chat entries, `"progress"` for ongoing synchronization status, and `"done"` to signal completion. The frontend registers distinct listeners for each event type to update specific UI components accordingly.