# How the Svelte 5 SPA in Agentsview Communicates with the Go Backend Using SSE for Real-Time Session Updates

> Discover how Agentsview's Svelte 5 SPA uses SSE to achieve real-time session updates from the Go backend. Learn about the EventSource connection and broadcaster pattern.

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

---

**The Svelte 5 SPA opens a native `EventSource` connection to the Go backend's `/api/v1/sessions/:id/watch` endpoint, which streams Server-Sent Events (SSE) through a broadcaster pattern that pushes session updates instantly to the browser.**

The **agentsview** repository pairs a Svelte 5 single-page application with a Go HTTP server to provide live visibility into AI agent sessions. To synchronize the frontend with backend state changes—such as new messages or timing updates—the Svelte SPA communicates with the Go backend using **Server-Sent Events (SSE)**, allowing the server to push data to the browser over a persistent HTTP connection without polling.

## Frontend: Establishing the SSE Connection

The SPA’s API layer in [`frontend/src/lib/api/client.ts`](https://github.com/kenn-io/agentsview/blob/main/frontend/src/lib/api/client.ts) exposes the `watchSession` function, which creates a native browser `EventSource` and wires event listeners for real-time updates.

### Creating the EventSource

Because the `EventSource` API does not support custom headers, the authentication token is appended as a query parameter to satisfy the Go server’s authorization requirements. This behavior is documented in [`docs/remote-access.md`](https://github.com/kenn-io/agentsview/blob/main/docs/remote-access.md).

```typescript
export function watchSession(
  sessionId: string,
  onUpdate: () => void,
  onTiming?: (t: SessionTiming) => void,
): EventSource {
  const url = `${getBase()}/sessions/${sessionId}/watch`;
  const token = getAuthToken();
  const fullUrl = token ? `${url}?token=${encodeURIComponent(token)}` : url;
  const es = new EventSource(fullUrl);  // Native SSE connection

  // Circuit-breaker logic
  let consecutiveErrors = 0;
  es.addEventListener("open", () => { consecutiveErrors = 0; });
  
  es.addEventListener("session_updated", () => {
    consecutiveErrors = 0;
    onUpdate();  // Trigger UI refresh
  });

  if (onTiming) {
    es.addEventListener("session.timing", (ev) => {
      onTiming(JSON.parse(ev.data) as SessionTiming);
    });
  }

  es.onerror = () => {
    consecutiveErrors += 1;
    if (consecutiveErrors >= WATCH_SESSION_MAX_CONSECUTIVE_ERRORS) {
      es.close();  // Prevent infinite reconnect loops
    }
  };
  
  return es;
}

```

The function returns the `EventSource` instance, which the SPA stores in [`frontend/src/lib/stores/events.svelte.ts`](https://github.com/kenn-io/agentsview/blob/main/frontend/src/lib/stores/events.svelte.ts). The store automatically closes the connection when the last subscriber disappears, ensuring the browser only maintains the stream while the session page is active.

## Backend: Registering the SSE Stream

In [`internal/server/huma_routes_sessions.go`](https://github.com/kenn-io/agentsview/blob/main/internal/server/huma_routes_sessions.go), the Go server registers the streaming endpoint using the Huma router’s stream helper, which configures the response for SSE by setting `Content-Type: text/event-stream` and disabling buffering.

```go
func (s *Server) registerSessionRoutes() {
    group := newRouteGroup(s.api, "/api/v1", "Sessions")
    // ...
    stream(s, group, http.MethodGet,
        "/sessions/{id}/watch", "Watch session events", s.humaWatchSession)
}

```

### The humaWatchSession Handler

The `humaWatchSession` handler (located in the same file) validates the session ID, checks permissions, and subscribes the HTTP connection to the global broadcaster. It writes RFC-compliant SSE frames—consisting of an `event:` line, a `data:` line, and a double newline terminator—to the `http.ResponseWriter`.

```go
func (s *Server) humaWatchSession(ctx context.Context, in *watchSessionInput) error {
    // 1. Resolve session and auth
    events, unsubscribe := broadcaster.Subscribe(in.ID)
    defer unsubscribe()

    // 2. Access underlying ResponseWriter and Flusher
    w := huma.ResponseWriterFrom(ctx)
    flusher := w.(http.Flusher)

    // 3. Stream loop
    for {
        select {
        case ev := <-events:
            fmt.Fprintf(w, "event: %s\n", ev.Event)
            fmt.Fprintf(w, "data: %s\n\n", ev.Data)  // Double \n terminates frame
            flusher.Flush()  // Push immediately to client
        case <-ctx.Done():
            return nil  // Client disconnected
        }
    }
}

```

## The Broadcaster Pattern

The `broadcaster` struct in [`internal/server/broadcaster.go`](https://github.com/kenn-io/agentsview/blob/main/internal/server/broadcaster.go) manages per-session subscriptions using a thread-safe map of channels. When any part of the backend—such as the sync engine in [`internal/sync/engine.go`](https://github.com/kenn-io/agentsview/blob/main/internal/sync/engine.go) or message parsers—detects a state change, it broadcasts the event to all active listeners.

```go
type broadcaster struct {
    mu   sync.RWMutex
    subs map[string]map[chan SSEEvent]struct{} // sessionID → subscriber channels
}

func (b *broadcaster) Broadcast(sessionID string, ev SSEEvent) {
    b.mu.RLock()
    for ch := range b.subs[sessionID] {
        select {
        case ch <- ev:
        default:
            // Drop if subscriber is too far behind (respects WATCH_MAX_BEHIND)
        }
    }
    b.mu.RUnlock()
}

```

When `broadcaster.Broadcast` is called, the `humaWatchSession` handler receives the event on its subscribed channel and writes it to the SSE stream, delivering the update to the browser within milliseconds.

## End-to-End Data Flow

1. **User opens a session page**: The Svelte component mounts and calls `watchSession(sessionId, callback)`.
2. **Browser connects**: The `EventSource` initiates a GET request to `/api/v1/sessions/<id>/watch?token=...`.
3. **Go server subscribes**: `humaWatchSession` validates the token, calls `broadcaster.Subscribe(sessionID)`, and enters a streaming loop.
4. **State change occurs**: The sync engine or parser detects a new message and invokes `broadcaster.Broadcast` with `event: session_updated`.
5. **Client receives update**: The browser fires the `session_updated` event, triggering the `onUpdate` callback and refreshing the UI.
6. **Cleanup**: If the connection drops, the browser auto-reconnects; after five consecutive errors, the frontend circuit-breaker closes the stream to prevent infinite loops.

## Summary

- **Native EventSource**: The Svelte 5 SPA uses the browser's built-in `EventSource` API to maintain a persistent connection to `/api/v1/sessions/:id/watch`.
- **Token-in-URL**: Because `EventSource` cannot set the `Authorization` header, the JWT token is passed as a query parameter and validated by the Go backend.
- **Broadcaster Pattern**: The Go backend decouples HTTP handling from business logic via [`internal/server/broadcaster.go`](https://github.com/kenn-io/agentsview/blob/main/internal/server/broadcaster.go), which multicasts updates to all active session viewers.
- **Circuit-Breaker**: Frontend logic in [`frontend/src/lib/api/client.ts`](https://github.com/kenn-io/agentsview/blob/main/frontend/src/lib/api/client.ts) prevents infinite reconnection attempts by closing the stream after `WATCH_SESSION_MAX_CONSECUTIVE_ERRORS` failures.

## Frequently Asked Questions

### Why is the authentication token passed as a query parameter instead of a header?

The native `EventSource` API does not allow setting custom HTTP headers, including the `Authorization` header typically used for JWT tokens. As documented in [`docs/remote-access.md`](https://github.com/kenn-io/agentsview/blob/main/docs/remote-access.md), the agentsview implementation appends the token to the URL as `?token=...` and validates it on the server side before establishing the SSE stream.

### How does the application handle temporary network interruptions?

The browser's `EventSource` implementation automatically attempts to reconnect when the connection drops. On the frontend, a circuit-breaker pattern tracks consecutive errors via the `onerror` handler; if `WATCH_SESSION_MAX_CONSECUTIVE_ERRORS` is reached, the code explicitly calls `es.close()` to stop retrying and prevent endless reconnect loops against a permanently failed endpoint.

### What happens to the SSE connection when the user navigates away from the session page?

The SPA manages connection lifecycle through [`frontend/src/lib/stores/events.svelte.ts`](https://github.com/kenn-io/agentsview/blob/main/frontend/src/lib/stores/events.svelte.ts). When the last Svelte store subscriber unsubscribes—which occurs when the user leaves the session page—the store closes the `EventSource`, immediately freeing the HTTP connection and removing the subscriber from the Go broadcaster's internal map.

### How does the backend support multiple users watching the same session simultaneously?

The `broadcaster` struct in [`internal/server/broadcaster.go`](https://github.com/kenn-io/agentsview/blob/main/internal/server/broadcaster.go) maintains a map of `sessionID` to a set of subscriber channels. When `humaWatchSession` handles a new request, it registers a unique Go channel for that connection. The `Broadcast` method iterates over all channels for the specific session ID, pushing the same SSE frame to every active viewer independently.