Critical caveat — API keys
Never ship your Boundary API key to the browser. Bundle-time env vars likeNEXT_PUBLIC_*, VITE_*, and REACT_APP_* are visible in page source and devtools. Either:
- Keep contracts server-side. Call your server, have the server invoke the contract + logger, and return the result. This is the recommended path.
- Use a custom
writethat POSTs to your authenticated server. The server then forwards to Boundary with the real key.
/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
WithflushOnExit: true (default), the SDK attaches two listeners:
visibilitychange— firesflush()whendocument.visibilityStatebecomes"hidden"(tab switched away, phone screen locked, tab about to be killed).pagehide— firesflush()when the page is being unloaded. Covers cases wherebeforeunloaddoesn’t fire (mobile Safari, navigating away from bfcache).
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 intoinputs: 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.
sendBeacon for final flush
The SDK usesfetch 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:
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. Thevisibilitychange / pagehide listeners only fire on actual tab/page lifecycle events.
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). Neitherdocument nor window is available, so the visibilitychange / pagehide listeners don’t attach — the SDK silently feature-detects. You drain explicitly:
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
Custom sinks
The write() pattern for proxying via your server
Capture policy
Why raw inputs/outputs must stay off client-side