Every AI product we've shipped this year has one architectural rule in common: the model provider key never touches the client. Not once, not for demos, not for prototypes. Every call goes through an edge function proxy, and every response is validated against a schema before the client sees it. Here's why, and how.
Why not the client
Because a client-side key is a public key. Every browser, every mobile app bundle, every desktop binary — the key ships along with the code. Someone opens dev tools, sees the network request, copies the header, and now they have your Anthropic account. It's not paranoia; it's the default state of a shipped app.
The proxy pattern
One edge function per external service. The function reads the secret from the server environment (Supabase secrets, Vercel env vars, Cloudflare bindings — pick one) and forwards a shaped request to the provider. The client calls the function with the anon key, never the provider key.
On Supabase, that means deploying with --no-verify-jwt so the client can call with the anon key. On Vercel, it means a route handler that reads process.env.ANTHROPIC_API_KEY inside the handler (never at module scope — that breaks builds without the env).
Structured output or bust
AI responses are text. Text is a nightmare to consume in code. Every one of our Claude calls uses an output schema — an enum for classifications, a shape for summaries, a strict object for anything the UI will render. If the model returns something off-shape, we parse it, catch the failure, and fall back to a safe default. The client never sees a raw model string.
Refusals are a happy path
Claude will sometimes refuse — safety, ambiguity, whatever. That's fine. Every function has an explicit refusal path that returns a neutral result and logs the incident. Users see a graceful default; we see the trend in logs and adjust the prompt.
Treat the AI like an external service that will fail. Not because it's bad — because everything fails eventually, and you want the fallback to be boring.
What this buys you
Security (one secret, one server). Auditability (every call is logged in one place). Cost control (rate-limit the proxy, not the client). And, most importantly, the ability to swap providers without touching the app — because the app talks to your function, and your function talks to whichever model you like.