React / Next.js App Router

How to hide your OpenAI API key in a React app

You can't hide a key inside React code: every VITE_ or REACT_APP_ variable is compiled into the JavaScript your visitors download. Move the OpenAI call to a server route (a Next.js route handler or a serverless function) and keep only a revocable KeyVault Edge token there, so the real key never ships and the token has hard limits.

Calling the OpenAI API directly from React browser code ships your sk-proj- key in the JavaScript file every visitor downloads, and scanners look for exactly that. A server-side route keeps the key out of the bundle; KeyVault Edge keeps it out of your server too and puts limits on the token that replaces it.

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.

TypeScript
// ❌ 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.

TypeScript
// ✅ 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

1

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.

2

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).

Shell
# .env.local — note: no NEXT_PUBLIC_ prefix
KVE_TOKEN=kvt_...your_token
APP_ORIGIN=https://yourdomain.com  # one of the token's allowed hosts
3

Call /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.

4

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.

5

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.

Shell
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.