How to hide your OpenAI API key in a React app
The Problem
This is what most React codebases look like today. The OPENAI_API_KEY key (sk-proj-...) lives directly in the process environment. If your repo, your CI logs, or your deployment config leaks — so does your key.
// ❌ DANGER: This React component ships your OpenAI key to every browser visitor
// components/ChatWidget.tsx
import { useState } from "react";
import OpenAI from "openai"; // bundled into your client JS!
// VITE_OPENAI_API_KEY is exposed in your JS bundle — anyone can extract it
const openai = new OpenAI({
apiKey: import.meta.env.VITE_OPENAI_API_KEY,
dangerouslyAllowBrowser: true, // red flag right in the name
});
export function ChatWidget() {
const [reply, setReply] = useState("");
async function sendMessage(text: string) {
const completion = await openai.chat.completions.create({
model: "gpt-4o",
messages: [{ role: "user", content: text }],
});
setReply(completion.choices[0].message.content ?? "");
}
return <button onClick={() => sendMessage("Hello")}>Chat — {reply}</button>;
}The Fix
Send the request to the KeyVault Edge proxy at https://proxy.keyvaultedge.com with the provider's usual path, your kvt_ token in X-KV-Token and the provider's base URL in X-KV-Target. The proxy refuses requests whose Origin isn't on the token's allowed hosts, then injects your real OPENAI_API_KEY, so it never sits in your app's environment.
// ✅ SAFE: API call lives only on the server; browser never sees any key
// app/api/chat/route.ts (Next.js App Router server route)
export async function POST(req: Request) {
const { messages } = await req.json();
const response = await fetch("https://proxy.keyvaultedge.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-KV-Token": process.env.KVE_TOKEN!, // kvt_... token, server-side only
"X-KV-Target": "https://api.openai.com",
// Must match one of the token's allowed hosts
Origin: process.env.APP_ORIGIN!,
},
body: JSON.stringify({ model: "gpt-4o", messages }),
});
const data = await response.json();
return Response.json(data.choices[0].message);
}
// components/ChatWidget.tsx (React client component — no keys here)
"use client";
import { useState } from "react";
export function ChatWidget() {
const [reply, setReply] = useState("");
async function sendMessage(text: string) {
const res = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ messages: [{ role: "user", content: text }] }),
});
const data = await res.json();
setReply(data.content ?? "");
}
return <button onClick={() => sendMessage("Hello")}>Chat — {reply}</button>;
}Setup in 5 steps
Move your OpenAI call off the client immediately
Create app/api/chat/route.ts in your Next.js project. This is a server-only file — no browser JS bundle will ever include it. All keys stay here.
Create a sanitized KVE token and add it server-side
At keyvaultedge.com, create a token for OpenAI. Set the allowed host to your deployment domain. Add KVE_TOKEN to Vercel environment variables (not NEXT_PUBLIC_ — that would be public).
# .env.local — note: no NEXT_PUBLIC_ prefix
KVE_TOKEN=kvt_...your_token
APP_ORIGIN=https://yourdomain.com # one of the token's allowed hostsCall /api/chat from your React component
Your React component calls its own /api/chat endpoint using fetch. No OpenAI SDK, no API keys, no dangerouslyAllowBrowser. The browser only talks to your own server.
Cap what the token can do
On the token, set requests per minute, hour and day, a monthly cost cap (estimated from the usage OpenAI reports), and restrict the endpoint scope to POST /v1/chat/completions. A leaked token then can't run up more than the cap, and you can revoke it in one click.
Verify no keys appear in your JS bundle
Run `npx next build` and inspect the .next/static/chunks/ directory with `grep -r 'sk-proj' .next/static/` to confirm no OpenAI key appears in the browser bundle.
npx next build
# Should return no output (no keys in the bundle)
grep -r "sk-proj" .next/static/What this protects, and what it doesn't
- Your real OPENAI_API_KEY stays at the edge, so it can't leak from your repo, logs, bundle or server environment.
- The kvt_ token is still a credential. A copied token can be replayed by anyone who can send the right headers, including a fake Origin from a server. The protection is that each token has its own rate limits, monthly cost cap and endpoint scope, and you can revoke it in one click without rotating the provider key.
- The cost cap is estimated from the usage the provider reports, so a request already in flight can overshoot it slightly.
More on layered defences in how to protect your OpenAI API key. Plans and limits are on the pricing page.
Start protecting your keys
Your OpenAI key is one leaked .env file away from being exploited. With KeyVault Edge the real key stays at the edge, and the token that replaces it is capped, scoped and revocable in one click.