> ## Documentation Index
> Fetch the complete documentation index at: https://docs.westyx.cloud/llms.txt
> Use this file to discover all available pages before exploring further.

# Go SDK - SSE Live Updates

> RunStream semantics, reconnection logic, and 429 handling for the Go SDK.

The SDK can hold a persistent `GET /v1/stream` connection open in a goroutine. Whenever the Nexus backend emits a change event (`config.updated`, `flag.toggled`, `secret.created`, `resync`, ...), the SDK triggers an immediate full sync - so the cache is updated within milliseconds of the change, instead of waiting for the next TTL tick.

## Auto-start

`NewClient` starts `RunStream` automatically in a background goroutine. No manual call is needed.

The goroutine blocks (reading the SSE stream and triggering syncs) until either:

* The context passed to `NewClient` is cancelled
* `client.Close()` is called

In a typical service, `defer client.Close()` is the only shutdown call needed - it cancels the internal stream goroutine and cleans up file-type secret temp files.

## What happens under the hood

1. The SDK opens an HTTP streaming response to `<BaseURL>/v1/stream` with `Accept: text/event-stream` and either `X-Nexus-API-Key: <key>` or `Authorization: Bearer <session_jwt>` (when [WIF](workload-identity) is enabled).
2. The server immediately sends a `resync` event, which causes the SDK to call `Sync(ctx)` once to align with the current state.
3. The connection stays open. Every change event triggers another full sync.
4. While the stream is up, the effective polling TTL is bumped to 60 s as a safety net.

The streaming HTTP client is a **separate instance** from the one you pass via `Config.HTTPClient` - the SDK clones your client at construction time and zeros the `Timeout`. This is why you can use a familiar `http.Client{Timeout: 10*time.Second}` for `/sync` without that timeout killing the long-lived SSE connection. See [Configuration](configuration#two-clients-under-the-hood).

## Observer hooks

Pass a `StreamObserver` via `Config.Observer` to receive structured callbacks for every SSE lifecycle transition (`OnConnected` / `OnDisconnected` / `OnEvent(name)` / `OnReconnectAttempt(n)` / `OnFallback(reason)` / `OnQuarantined(reason, expiresAt)`). See [Stream observer](stream-observer) for the full contract.

## `auth_expiring` control event (WIF)

When [WIF](workload-identity) is enabled, the backend emits an `auth_expiring` SSE control event \~5 minutes before the session JWT expires. The SDK reacts by triggering a non-blocking session refresh - it does NOT call `/sync`, and the event is NOT surfaced through `OnEvent`. When the server force-closes the stream at expiry, the SDK reconnects with the freshly-issued bearer.

## Reconnection

Any transport error (server restart, TCP drop, network blip) triggers a reconnect with **exponential backoff**: 1s - 2s - 4s - 8s - 16s - 30s (capped at 30s).

After **three consecutive transport errors** the SDK sleeps the first reconnect-schedule interval then reconnects SSE automatically. By default the schedule progresses **5 → 10 → 20 → 40 → 60 minutes** (capped at 60 min on each subsequent fallback). The `Observer.OnFallback` hook fires when the stream enters this state.

Configure a different schedule with `SSEReconnectCooldown` in `Config`:

```go theme={null}
nexus.Config{
    // ...
    SSEReconnectCooldown: []int{5, 10, 30}, // custom: 5 min, 10 min, then 30 min fixed
}
```

The reconnect counter resets to 0 after every successful SSE connection. The manual re-launch pattern from earlier SDK versions is no longer needed.

## `429 Too Many Requests`

Every project tier has a per-service stream connection limit:

| Tier   | Connections |
| ------ | ----------- |
| `free` | 5           |
| `xs`   | 10          |
| `s`    | 25          |
| `m`    | 50          |
| `l`    | 150         |
| `xl`   | 500         |

Exceeding the limit returns `429 Too Many Requests`, typically with a `Retry-After` header indicating when the client should try again.

### Behaviour (v0.6.0+)

The SDK applies a presence-conditional refinement on `Retry-After`:

* **With a parseable `Retry-After` header** (delta-seconds): the SDK parses the value, clamps it to `[5 s, 5 min]`, fires `OnFallback(FallbackReasonRateLimited)` once for observability, then **sleeps the indicated delay and reconnects automatically** inside `RunStream`. The reconnect attempt does **not** increment the transport-failure counter - `Retry-After` is a cooperative throttle from the server, not a transport error, so it does not consume one of the three reconnect strikes that lead to `MAX_FAILURES` fallback.
* **Without a parseable `Retry-After` header**: the SDK fires `OnFallback(FallbackReasonRateLimited)` then schedules an SSE reconnect using the `SSEReconnectCooldown` schedule (default 5 → 10 → 20 → 40 → 60 minutes). No manual recovery is needed.

The clamp floor of **5 s** prevents a hostile or buggy server from inducing reconnect spam via a `Retry-After: 1`. The ceiling of **5 min** prevents a malicious or accidental large value from stalling the live-update channel indefinitely.

```go theme={null}
obs := &myObserver{}
// ...
// When the server sends 429 + Retry-After: 30, the SDK fires
// obs.OnFallback("rate-limited") once, sleeps ~30s honoring the header,
// then reconnects. The failures counter is NOT incremented.
```

The sleep respects context cancellation - if `ctx` is cancelled during the wait, `RunStream` returns promptly instead of waiting for the full `Retry-After` window.

## SSE is always on

`NewClient` always starts the stream goroutine. Pure TTL-polling mode is not supported - the goroutine runs until the context is cancelled or `Close()` is called.

## Lifecycle

The stream goroutine is bound to the context passed to `NewClient`. Cancelling that context or calling `client.Close()` stops it. `Close()` is the preferred shutdown signal - it cancels the stream and cleans up temp files.

```go theme={null}
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer cancel()

client, _ := nexus.NewClient(ctx, cfg)
defer client.Close()

// ... run service ...

<-ctx.Done() // wait for shutdown signal
// Close() will stop the stream goroutine
```

## What events trigger a sync

| Event                                                 | secret keys | public keys          |
| ----------------------------------------------------- | ----------- | -------------------- |
| `resync`                                              | yes         | yes                  |
| `config.created` / `.updated` / `.deleted`            | yes         | yes                  |
| `flag.created` / `.updated` / `.toggled` / `.deleted` | yes         | yes                  |
| `secret.created` / `.updated` / `.deleted`            | yes         | filtered server-side |

<Note>
  The SDK does **not** apply event payloads directly to the cache - every event triggers a full re-sync that returns the current authoritative state. This avoids any drift between the SDK's view and the backend.
</Note>
