Keep the key on the server
Anything shipped to the browser is public. An API key in client code, in an env variable exposed to the bundle or in a request header can be read from DevTools in seconds - and the key is a direct line to your bill.
What goes wrong
- Key theft - someone copies it and runs their own workloads on your account.
- No control over usage - you cannot rate-limit per user, block abusers or cap spending.
- No control over the prompt - the client decides the system prompt, the model and
max_tokens, so anyone can turn your "support bot" into a free general-purpose model. - Leaked env vars - in Next.js anything prefixed with
NEXT_PUBLIC_is inlined into the client bundle.
The right shape: a thin proxy
ts
// app/api/chat/route.ts - runs on the server, the key never leaves it
export async function POST(req: Request) {
const user = await getUser(req); // auth first
if (!user) return new Response('Unauthorized', { status: 401 });
await rateLimit(user.id); // per-user limits
const { messages } = await req.json();
const upstream = await fetch(LLM_API_URL, {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.LLM_API_KEY}` },
body: JSON.stringify({
model: MODEL, // chosen by the server
system: SYSTEM_PROMPT, // not by the client
max_tokens: 1024,
messages: validate(messages),
stream: true,
}),
});
return new Response(upstream.body, { headers: { 'Content-Type': 'text/event-stream' } });
}The browser talks only to /api/chat. The server owns the key, the model choice, the system prompt, limits and logging.
The exception is ephemeral, scoped tokens that some providers issue for realtime/voice APIs: the server mints a short-lived token and the browser uses it. Your long-lived key still never leaves the server.
updated
did you know it?