OpenAI or Stripe API Key Exposed in Frontend Code? Why It Happens and How to Fix It

You asked Lovable, Bolt, Cursor, Claude, Grok or ChatGPT to "add AI chat" or "add Stripe payments". It worked first time. Then someone in a Discord channel pointed out that your API key is sitting in your site's JavaScript where anyone can read it.

This is one of the most common mistakes in vibe-coded apps, and it isn't really a bug. It's how frontend build tools are designed to work. Here's what's going on and how to fix it properly.

Why your key ends up in the browser

Frontend frameworks have a convention for marking environment variables as safe to make public:

At build time, the value of any variable with one of these prefixes is copied as plain text into the JavaScript your visitors download. So this:

const openai = new OpenAI({
  apiKey: import.meta.env.VITE_OPENAI_API_KEY,
  dangerouslyAllowBrowser: true,
});

ships your real key to every visitor. (The OpenAI SDK even makes you set dangerouslyAllowBrowser to do it. That name is a warning.)

AI assistants often reach for this pattern because the browser code needs the key, the build fails without the prefix, and adding VITE_ makes the error go away. Nothing breaks, so you don't notice.

OpenAI's own guidance is clear: exposing your key in a browser or mobile app lets other people make requests on your behalf, which can mean unexpected charges, and requests should be routed through your own backend.

What a leaked key costs you

How to check if you're exposed

  1. Search your code for public-prefixed secrets:
    grep -rnE "(VITE|NEXT_PUBLIC|PUBLIC)_[A-Z_]*(OPENAI|ANTHROPIC|STRIPE_SECRET|SECRET|SERVICE_ROLE)" src/ .env* 2>/dev/null
    
  2. Search for raw key formats: sk- (OpenAI-style), sk_live_ and rk_live_ (Stripe secret and restricted keys).
  3. Check what you actually shipped: open your live site, go to DevTools → Sources (or Network), and search the loaded JavaScript for sk- or sk_live.
  4. Check your git history, not just today's files. A key you removed last week is still in old commits.

Step 1: Revoke and replace the key

Do this before you change any code. The key has already been served to browsers and may have been scraped, and deleting it from your source doesn't change that.

OpenAI: go to the API keys page in the OpenAI platform, delete the exposed key, and create a new one. Check the Usage page for activity you don't recognise, and review your organisation's members.

Stripe: on the API keys page open the key's ⋯ menu and choose Rotate key. Stripe lets the old key keep working for up to 7 days so you can update your servers, but for a key that is public, set the expiration to Now. Put the new key in your server environment, then review recent payments, refunds, payouts and any newly created API keys or webhooks. Stripe now recommends restricted keys (rk_live_…) with only the permissions your server needs instead of the all-powerful secret key.

Step 2: Move the call to a server

The rule: any key that costs money or grants access lives only on a server. Your frontend calls your endpoint, and your endpoint calls OpenAI or Stripe.

In a Lovable or Supabase-based app, a Supabase Edge Function works well:

// supabase/functions/chat/index.ts — runs on the server
import OpenAI from "npm:openai";

Deno.serve(async (req) => {
  // 1. Check the user is logged in (verify their Supabase JWT) before spending money
  const { messages } = await req.json();
  const openai = new OpenAI({ apiKey: Deno.env.get("OPENAI_API_KEY") }); // no VITE_ prefix
  const result = await openai.chat.completions.create({
    model: "gpt-4o-mini",
    messages,
  });
  return Response.json(result);
});

Set OPENAI_API_KEY as a function secret, then call the function from the browser with supabase.functions.invoke("chat", { body: { messages } }).

In Next.js, use a route handler such as app/api/chat/route.ts and read process.env.OPENAI_API_KEY. Without the NEXT_PUBLIC_ prefix, it stays on the server.

For Stripe, the browser should only ever:

Prices and amounts should be decided on the server, never taken from the browser. Otherwise a user can edit the request and pay $0.01.

Step 3: Don't just move the leak

A server endpoint with no protection is a leaked key with extra steps. Anyone can call it and spend your credit. Add:

Paste-ready fix prompt

"Find every API key used in frontend code. Remove any secret (OpenAI, Anthropic, Stripe secret key, Supabase service role) from the browser and from any VITE_ or NEXT_PUBLIC_ variable. Create a server-side endpoint (Supabase Edge Function or API route) that reads the key from a server-only environment variable, requires an authenticated user, and applies rate limiting. Update the frontend to call that endpoint. Keep only the Stripe publishable key and Supabase anon key in the browser."

After the AI makes the change, check the diff yourself and search the built bundle again.

Find exposed keys before someone else does

Run a free ShipCheck scan on your GitHub repo (public, or private via Connect GitHub). ShipCheck uses gitleaks to find OpenAI, Stripe and other keys in your current code and your last 100 commits, flags .env and .gitignore mistakes, and checks your dependencies for known vulnerabilities. You get a plain-English 0–100 score and a list of what to fix first.

Built it with Lovable, Bolt, Cursor, Claude, Grok or ChatGPT? Is your repo safe to ship?

Paste a public GitHub repo, or connect GitHub for a private one, and get a risk score in under a minute. Free.

Scan your repo free →

Built it with Lovable, Bolt, Cursor, Claude, Grok or ChatGPT? Is your repo safe to ship?

Paste a public GitHub repo, or connect GitHub for a private one, and get a risk score in under a minute. Free.

Scan your repo free →