Every chat-style Claude product has the same three-hop architecture: the model streams to your backend, your backend streams to the browser, and the browser renders fragments as they land. The middle hop is yours to build, and the almost-universal choice for it is Server-Sent Events (SSE) — the same protocol Claude streaming itself uses on the wire. This article explains the proxy pattern and the traps to avoid.
Why a proxy at all?
Because credentials must never reach the browser. Claude API keys are long-lived secrets sent in the x-api-key header; official guidance is to keep them in a secrets manager, rotate them periodically, and revoke on suspected leak. A key embedded in browser JavaScript is a key published to the world. The TypeScript SDK enforces the point by disabling browser execution by default. On third-party platforms the same logic holds with different credentials — AWS SigV4 signing on Claude Platform on AWS or Bedrock, Google Cloud Application Default Credentials on Vertex AI, a resource API key on Microsoft Foundry. None of these belong in a browser either.
So the browser talks only to your backend, authenticated with your app's session mechanism. The backend holds the platform credentials, calls Claude with the SDK, and relays the stream onward.
The relay pattern
The backend route does four things:
1. Authenticate the user. Your normal session check — this endpoint spends your token budget, so treat it like any other billable API and apply rate limiting per user.
2. Open the Claude stream. Using the SDK's streaming interface (see streaming basics), start the request server-side. On Claude Platform on AWS the upstream is already SSE, same as the first-party API, which makes the mental model pleasantly uniform.
3. Set SSE response headers. Respond to the browser with content type text/event-stream, and disable buffering in anything between you and the client. This is the classic failure mode: the code is correct, but a reverse proxy, load balancer, or CDN buffers the response and the "stream" arrives as one lump at the end. Most proxies have an explicit switch to exempt event-stream responses.
4. Forward fragments as events. Loop over the SDK stream and write each text fragment to the response as an SSE data: line. You rarely want to forward the upstream events verbatim — instead, translate to your own minimal event vocabulary (for example text, done, error), so your frontend contract is stable even if you later switch platforms or add server-side processing such as moderation or citation handling.
On the browser side, the built-in EventSource API or a small fetch-based reader consumes the stream and appends text to the UI as it arrives. No SDK is needed in the browser at all — that is the point.
x-api-key or a model name, the boundary is drawn in the wrong place.Failure handling across two streams
You now have two connections that can each fail independently. Handle both directions:
Upstream dies mid-generation. Send the browser an explicit error event rather than silently ending the stream — the frontend can then show a retry affordance next to the partial text. Log the assembled partial message server-side, including the request IDs your platform returns, so support investigations have something to hold onto.
The browser disconnects. Detect the closed client connection and abort the upstream Claude request. Every token generated after the user closed the tab is still billed; on a busy product, orphaned streams are a real cost line.
Reconnection. Native EventSource auto-reconnects and replays from its last event ID — which does not magically resume a model generation. Either disable auto-reconnect and treat each generation as one-shot, or design your event IDs so a reconnect starts a fresh request cleanly.
Where to go next
The TypeScript streaming article covers the server-side iterator you will loop over; streaming helpers covers capturing the final message for logs while you relay fragments. For platform setup, start at the quickstart.