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

# Browser

> Client-side contracts with pagehide + visibilitychange drain

The SDK runs in the browser, so you can wrap client-side LLM calls in a contract and watch them on the dashboard the same way you watch your server-side traffic.

## When you'd want this

* **Streaming chat UIs.** Validate the model's structured suggestions (action proposals, function calls) before rendering them — and see on the dashboard which prompts cause the most retries from real users.
* **Copilots that surface model output.** A code suggester or writing assistant runs the same correctness loop in the browser; the SDK shows you which rules fire and which models recover fastest.
* **A/B testing prompt or model variants.** Group runs by `environment: "production-variant-b"` and compare acceptance rate side-by-side without standing up a separate logging pipeline.
* **Lightweight local validation.** Tools where you can't (or don't want to) round-trip to a server still get the same retry/repair behavior — and now you can measure it.

## Critical caveat — API keys

**Never** ship your Boundary API key to the browser. Bundle-time env vars like `NEXT_PUBLIC_*`, `VITE_*`, and `REACT_APP_*` are visible in page source and devtools. Either:

1. **Keep contracts server-side.** Call your server, have the server invoke the contract + logger, and return the result. This is the recommended path.
2. **Use a custom `write` that POSTs to your authenticated server.** The server then forwards to Boundary with the real key.

```typescript theme={null}
// client-side — no apiKey, no direct Boundary traffic
const logger = createBoundaryLogger({
  async write(events) {
    await fetch("/api/boundary-proxy", {
      method: "POST",
      body: JSON.stringify(events),
      credentials: "include",
    });
  },
});
```

Then in `/api/boundary-proxy`, authenticate the user and forward to Boundary with the server-side key. You get browser-origin telemetry without exposing the key.

## What the SDK does on the browser

With `flushOnExit: true` (default), the SDK attaches two listeners:

* `visibilitychange` — fires `flush()` when `document.visibilityState` becomes `"hidden"` (tab switched away, phone screen locked, tab about to be killed).
* `pagehide` — fires `flush()` when the page is being unloaded. Covers cases where `beforeunload` doesn't fire (mobile Safari, navigating away from bfcache).

Neither attaches to `beforeunload` specifically — mobile browsers are inconsistent about firing it, and `pagehide` is the modern guidance.

## Best-effort only

If the browser is terminating the tab (user force-quits, OS kills the process), the flush has no guarantee of completing. The SDK uses whatever time the browser grants before the page goes away. For events you must not lose, ship them server-side.

## Tighten the capture policy

The browser bundle is user-visible. Never opt into `inputs: true` or `outputs: true` in a client-side logger — the raw prompt and completion become part of data the user's browser receives over the wire, and then sends back.

```typescript theme={null}
createBoundaryLogger({
  write,
  capture: {
    inputs: false,
    outputs: false,
    repairs: true,
  },
  // redact anything a rule could have interpolated
  redact: {
    patterns: [
      /[\w.+-]+@[\w-]+\.[\w.-]+/,
      /\b\d{3}-\d{2}-\d{4}\b/,
    ],
  },
});
```

## sendBeacon for final flush

The SDK uses `fetch` under the hood, which can be aborted mid-flight by page navigation. A `write` sink that uses `navigator.sendBeacon` instead is more reliable for the final flush:

```typescript theme={null}
const logger = createBoundaryLogger({
  write(events) {
    const blob = new Blob([JSON.stringify(events)], { type: "application/json" });
    // Works in pagehide / visibilitychange handlers where fetch() often cancels.
    const ok = navigator.sendBeacon("/api/boundary-proxy", blob);
    if (!ok) {
      // Falls back to fetch if the beacon queue is full.
      return fetch("/api/boundary-proxy", {
        method: "POST",
        body: blob,
        keepalive: true,
      }).then(() => undefined);
    }
  },
});
```

`sendBeacon` is fire-and-forget — no response, no retry. That's fine for telemetry but means you can't assert delivery.

## Single-page apps

Keep the logger at module scope. A route change inside an SPA is not a page unload, so the logger survives naturally. The `visibilitychange` / `pagehide` listeners only fire on actual tab/page lifecycle events.

```typescript theme={null}
// src/boundary.ts
import { createBoundaryLogger } from "@withboundary/sdk";

export const logger = createBoundaryLogger({
  write: sendToProxy,
  batch: {
    size: 10,
    intervalMs: 2000,   // faster flush — browsers die more often than servers
    maxQueueSize: 100,
  },
});
```

Lower `maxQueueSize` than Node defaults — memory pressure matters more in the browser, and dropping old events on an unload storm is acceptable.

## Web Workers

The SDK runs in Web Workers (Dedicated or Shared). Neither `document` nor `window` is available, so the `visibilitychange` / `pagehide` listeners don't attach — the SDK silently feature-detects. You drain explicitly:

```typescript theme={null}
// worker.ts
self.addEventListener("message", async (e) => {
  const result = await contract.accept((attempt) => runLLM(attempt));
  self.postMessage(result);
  await logger?.flush(500);
});
```

## Streaming LLM responses

If your client streams from the model (SSE / fetch streaming), the contract still runs after the stream completes — `RunFn` returns the assembled string. There's no special browser wiring; the contract + logger operates the same way as in Node.

## See also

<CardGroup cols={2}>
  <Card title="Custom sinks" icon="code" href="/sdk/custom-sinks">
    The write() pattern for proxying via your server
  </Card>

  <Card title="Capture policy" icon="filter" href="/sdk/capture-policy">
    Why raw inputs/outputs must stay off client-side
  </Card>
</CardGroup>
