An OpenAI key in the frontend is published, not protected
This is the single most common expensive mistake in AI-built apps: an OpenAI API key placed in a client-exposed variable so the browser can call OpenAI directly. The key has a recognizable prefix, sk- or the newer sk-proj-, and once it is wired into a variable like VITE_OPENAI_API_KEY or NEXT_PUBLIC_OPENAI_API_KEY, the build inlines it into the JavaScript that ships to every visitor. At that point the key is not hidden behind a clever attack. It is published.
The reason this one costs money is that OpenAI charges per request. A Supabase anon key returning rows is a data problem; a billing-capable OpenAI key in the bundle is a spend problem. Anyone who reads the key can send requests to OpenAI as your account, and the usage lands on your invoice.
- Observable: the sk- or sk-proj- string sitting in a loaded .js file, or a request going straight from the browser to the OpenAI API host (api.openai.com) with the key in an Authorization header. If your browser can see it, so can anyone who opens DevTools.
- Only possible until you check: nothing about exposure is conditional. A key that ships in the bundle is reachable by anyone the moment the URL goes public. Treat it as compromised from that point on, not as something that might leak later.
Why automated harvesting makes this immediate
The dangerous part is not a single attacker reading your bundle by hand. It is that automated scanners continuously crawl public sources for key patterns. The sk- and sk-proj- shapes are well known, and harvesting of exposed credentials from public code and deployed bundles is a well-documented practice. A key that matches the pattern and sits on a public surface is found by tooling, not by luck.
We are deliberately not putting numbers on this. You will see claims that exposed keys get drained in some specific window for some specific dollar amount; treat those as illustrative, not measured. The honest framing is simpler and stronger: a publicly exposed OpenAI key should be treated as compromised the moment it ships, because the public sources that carry it are exactly the ones being crawled.
- The key pattern is matchable by anyone running a scanner, including ones that watch public repos and live sites continuously.
- A public URL is a public source. The bundle your deploy serves is reachable the same way a public GitHub file is.
- "Nobody knows my URL yet" is not protection. Discovery is automated, and a key that ships in the bundle does not need a human to notice it first.
Why AI tools generate the client-side pattern
When the prompt is "call OpenAI and show the result," the shortest path to a working feature is a direct call from the frontend with the key inlined. That call works on the first try, the UI lights up, and the app looks finished. The split between "this belongs in the browser" and "this must stay on the server" is the step that does not happen automatically.
The variable naming is the trap. A VITE_ or NEXT_PUBLIC_ prefix is an instruction to the bundler to embed the value in client code, not a safety label. Naming an OpenAI secret VITE_OPENAI_API_KEY or NEXT_PUBLIC_OPENAI_API_KEY tells the build to ship it. The generated code runs, a completion comes back, and the demo passes, so the wrong placement is invisible from inside the app. This is the same class of mistake covered in our note on Anthropic API keys exposed in the client bundle; the provider differs, the mechanism is identical.
- The feature works, so the key placement looks correct from the inside.
- The prefix is a publish instruction. Putting a secret behind it inlines the secret.
- Fast deploy means the public URL and the bundle exist before anyone audits which strings the build shipped.
Check your bundle for sk- and direct OpenAI calls
You can confirm this on your own app without any attack, because the bundle is already public. Open your deployed URL logged out, the way a first-time visitor arrives, and run two passes. For the full DevTools method, see our note on finding an API key in the browser network tab; this is the OpenAI-specific version.
From the browser: open DevTools, go to the Network tab, and reload so requests are captured. First, search the loaded sources for the key shape, the global search across all sources (Cmd-Shift-F in Chrome, Ctrl-Shift-F on Windows or Linux) for sk- and sk-proj-. Second, look at Fetch/XHR for any request whose URL is api.openai.com. A call going straight from the browser to the OpenAI host means the frontend is talking to OpenAI directly, which means the key it uses is in the client.
From a build artifact: run your production build locally and search the output folder (for example dist or .next) for sk- and sk-proj-. A command-line search across the build output is the fastest version of this check.
- Suspicious result: a string starting sk- or sk-proj- appears in any file the browser fetched, or you see requests going directly to api.openai.com from the page. Treat the key as compromised.
- Safer result: the only requests touching OpenAI go to your own backend, and no sk- string appears in any served file. The browser talking to your server, and your server talking to OpenAI, is the shape you want.
- Match on the prefix shape, not the full value. Do not paste a real key into this note, a screenshot, or a ticket while investigating.
Move the call server-side, then rotate the key
The fix has two parts, and both are required if the key ever shipped.
Move the OpenAI call behind a server route, API route, or edge function that holds the key in a server-only variable, one with no VITE_ or NEXT_PUBLIC_ prefix. The browser calls your endpoint, your endpoint calls OpenAI, and the key never enters the bundle. Then add the controls that a now-public endpoint needs: authentication so only your users can reach it, rate limiting so a single caller cannot flood it, and usage caps so a runaway loop cannot run up an unbounded bill. As a backstop, set spend limits in the OpenAI dashboard so the account itself has a ceiling, independent of your code.
If a search ever found sk- or sk-proj- in your public bundle, rotate that key immediately in the OpenAI dashboard before anything else. Exposure equals compromise: once a key has been published you cannot un-publish it, and you have no way to know who already copied it. Rotating issues a new key and invalidates the old one, after which you update only the server-side variable. Removing the key from your code without rotating leaves the old, already-public value valid.
- Server-only variable, no client prefix, read only by server code.
- The new endpoint needs auth, rate limiting, and usage caps; without them you have moved the spend risk, not removed it.
- OpenAI's account-level usage and spend limits are a backstop, not the primary control.
- If it shipped: rotate in the OpenAI dashboard first, then redeploy, then re-run the bundle check to confirm no sk- string remains.
Where VibeCodeGuard fits, and what it does not prove
VibeCodeGuard's Launch Check fetches your public bundle and flags strings matching OpenAI key patterns, the sk- and sk-proj- shapes. A match means an OpenAI key is reachable from the public surface, which is one of the clearest launch blockers a scan can surface, and the result comes with severity and the same fix direction: treat any flagged key as compromised and rotate it. It checks the thing an outside visitor can actually download.
What it does not do is review your server code. After you move the call server-side, the scan cannot confirm that your new endpoint checks who is calling it, enforces a rate limit, or caps usage. A clean bundle with an unauthenticated, uncapped proxy in front of OpenAI is still a spend risk, and that is manual review territory.
Next step: run your production build, search the output for sk- and sk-proj-, and confirm the only path to OpenAI runs through your own server. If anything starting sk- shows up, rotate it in the OpenAI dashboard now, then run a focused Launch Check against the public URL before sharing it.
A clean Launch Check means no sk- or sk-proj- string was found in the public bundle it fetched. It does not prove your OpenAI proxy is authenticated or rate limited, and it cannot see keys that live only in private repos, server logs, or build steps it did not observe. Treat a flagged key as a launch blocker and rotate immediately; treat a clean result as one signal, not a guarantee.
> launch check
Scan the public surface before launch.
Get severity, evidence, and practical fix guidance for the checks VibeCodeGuard can run from the outside.