Skip to content

Security basics

Your OpenAI API key is exposed in your frontend. Rotate it.

An OpenAI API key exposed in your frontend cannot be locked to a domain. Rotate it today, move the call behind your own endpoint, and cap the spend.

Vlad Tkachenko9 min read
A key-shaped value inside a page of app code, drawn long enough to run off the right edge of the pane holding it.

In short

  • An OpenAI API key exposed in your frontend is the case where the alarm is right. No setting makes one safe in a browser.
  • It is a bearer token, so holding it is the whole qualification. There is no domain restriction to lock it to your own site, the way there is for a Google key.
  • Rotate it in the OpenAI dashboard today, move the call behind an endpoint you own, and put a spend limit on the project.
  • We found one in 33 of 30,998 scanned apps. Rare, and all 33 came out graded D or F.

Open your app in a browser, view the page source, and search it for sk-proj-. If a long string comes back, your OpenAI API key is exposed in your frontend, and every visitor you have ever had could have copied it.

Here is the part that most advice on this gets wrong: an OpenAI key is not a Google key with a different prefix, and nothing you can set in a dashboard makes it safe where it is. A Google Maps key belongs in your page, and one free setting protects it. An OpenAI key has no equivalent setting anywhere, which is why the only honest first instruction is to rotate it.

Between 12 and 14 August 2026 we ran nine external checks over 30,998 live apps built with Lovable, Bolt, v0, Replit and Base44. An OpenAI key turned up in 33 of them. All 33 came back graded D or F, because a single critical finding caps the grade no matter what else the app got right. The full numbers are in our scan report.

Is an OpenAI API key exposed in your frontend a problem?

Yes. This is the one where the alarm is right.

An OpenAI key is a bearer token, and the word carries the whole explanation: whoever bears it may use it. It travels in a header that reads Authorization: Bearer sk-proj-…, and OpenAI's servers ask for nothing else. Not which website sent it. Not which country it came from, or whether the sender is you.

Think of a train ticket rather than a passport. A conductor does not check whose name is on a ticket, because holding it is the entire qualification. That is what makes a ticket worth stealing and a passport mostly not.

So a key printed into your app is a ticket you handed to every visitor. Most of them will never look. The ones who do are usually not people at all: automated scrapers crawl public pages collecting key-shaped strings, and they do not need to know who you are to find yours.

Why the environment variable did not hide it

Because a frontend build compiles environment variables into the file it ships.

This is the step that makes people confident the key is safe. You moved it out of your code and into a .env file, named it VITE_OPENAI_API_KEY or NEXT_PUBLIC_OPENAI_API_KEY, and now the code names a variable where the key used to be. Nothing about the shipped app changed. The build tool replaced that variable with its value on the way out, and the value is sitting in the JavaScript your visitor downloads.

Vite's documentation says so plainly: variables carrying the VITE_ prefix are exposed in client-side source code after bundling, and sensitive information like API keys should not be in one, because the values are bundled into your source. The VITE_ and NEXT_PUBLIC_ prefixes are not a safe box. They are a declaration that you understand this variable is public.

An environment variable did do one real thing for you: it kept the key out of your repository, where anyone reading your code would have met it. Your visitors never read your code. They read the file your build produced from it.

Why you cannot restrict it the way you restrict a Google key

Because OpenAI does not offer a restriction of that kind.

If you have read about a Google API key in a frontend, you met a five-minute fix: open the key in the Google Cloud console, set an HTTP referrer restriction, and the key printed in your page works on your site and returns an error everywhere else. That advice is correct for a Google key, and it does not carry across.

There is no field on an OpenAI key for "only from yourapp.com". No domain allowlist, no referrer check, and no IP restriction that survives a browser. What OpenAI gives you instead sits on the account behind the key: which project it belongs to, what that project is allowed to spend in a month, and whether the key still exists. Those limit what a stolen key can cost. The key itself goes on working from anywhere until you delete it.

The upper lane is the fix people are told about. In the lower lane there is nothing to draw, because OpenAI has no setting of that shape.

What dangerouslyAllowBrowser actually does

It switches off a guard, and its name is the documentation.

OpenAI's official JavaScript library refuses to run in a browser by default. The README says browser support is "disabled by default to avoid exposing your secret API credentials", and that enabling dangerouslyAllowBrowser "can be dangerous because it exposes your secret API credentials in the client-side code".

If your app calls OpenAI from the browser, that option is set to true somewhere in your code, because the library will not start without it. Somebody typed the word dangerously to make an error message go away. That is usually how this finding gets made, and the library did tell you first.

The option has real uses, and all of them are narrow: an internal tool where you know every user, a temporary development key, a key scoped so tightly that spending it costs nothing. A public app on the open internet is none of those.

What it costs when someone finds your key

A bill, and an app that stops working.

The bill is the part people expect. Somebody else's requests are charged to your account, and model calls are not cheap by the standards of a side project. The usual way an owner notices is an invoice larger than last month's for no reason they can point to.

The outage is the part they do not expect. Your account has rate limits, and a stranger's traffic spends them. Your own app starts returning errors during the hours somebody else is busy, which reads as a bug rather than as a theft, so people spend a day debugging the wrong thing.

There is a third cost, and it depends on how the key was scoped. An API key authenticates every call the project it belongs to can make, so a broadly scoped key reaches whatever else is stored there: files uploaded to the account, fine-tuned models, assistants you built. Check what the key could actually reach before you decide this was only about money.

How to call OpenAI from your app without shipping the key

Put something of yours between your visitor and OpenAI.

The shape is the same everywhere. Your app calls a small endpoint you own. That endpoint holds the key, calls OpenAI, and hands the answer back. The browser never sees the key, because the key never leaves your server.

The same request in both rows. What changes is which side of your own server the key is standing on.

Where that endpoint lives depends on what you built with:

  • Supabase in your stack: an Edge Function, with the key stored as a secret in the Supabase dashboard.
  • Deployed on Vercel or Netlify: a serverless function under /api, with the key in the project's server-side environment variables.
  • Lovable, Bolt or Replit: each has a secrets store of its own. The rule does not change, and neither does the trap: a store labelled secrets still ships the value to the browser if the code reading it runs there.

Two things are worth doing while you are in there. Give your endpoint a rate limit, because an endpoint that calls OpenAI for anybody who asks is the same bill by a slower route. And set a monthly spend limit on the OpenAI project, which is the one control that puts a floor under the worst case.

If your app uses OpenAI's Realtime API for voice, the browser genuinely does need a credential, and OpenAI documents how to give it one: your server mints a short-lived client secret and hands the page that. The ticket still exists, it expires in minutes, and your own server is what issued it.

How to find every key your app is shipping, for free

Start by hand, because it costs five minutes and needs nothing installed. View your live site's source and search it for sk-proj- for an OpenAI key, sk-ant- for an Anthropic one, AIza for Google, and eyJ for a Supabase token.

What that misses is the JavaScript the page loads afterwards, which on a vibe-coded app is nearly all of it. Our scanner opens your app in a real browser, waits for the bundles to arrive, and reads those instead of the HTML. It also decodes each Supabase token and reports the role written inside it, so a key that belongs in your page comes back marked correct rather than buried in a wall of red.

The key check is one of nine, and the other eight are why a grade tells you more than a search does:

The nine read-only checks a scan runs, in the order they are listed below. Every one of them is read from outside, the way a stranger sees your app.
What the scan looks atThe question it answers
Secret keys in your codeIs a paid-API or admin key readable by anyone who looks?
Database rulesCan a stranger read your users' rows without logging in?
Private filesAre .env files or database dumps downloadable from a URL?
Security headersAre the browser-side protections switched on?
Storage bucketsCan anyone list the files your users uploaded?
Source mapsIs your original source code published alongside the app?
Open APIs and CORSDo your endpoints answer any website that asks?
Certificate expiryIs HTTPS valid, and not about to lapse on your visitors?
Domain renewalIs the name renewed before somebody else can take it?

You get a grade, a score and the counts on screen in about 20 seconds, with no account. Give an email address and the detailed list comes with it, along with a fix written for your builder that you can paste straight in.

Three things it will not do, which are the reasons it is safe to point at a live app: it never logs in, it never writes anything, and it never keeps a key it finds. An exposed secret is stored as a masked hint of the form sk-proj-…a1b2, and the real value is discarded. Scan your app, or read what each of the nine checks looks at first.

What to do right now

What to do

  • Rotate the key first, in the OpenAI dashboard under API keys. Deleting it from your code closes nothing, because the old value is still in your version history and in every cached copy of your page.
  • Set a monthly spend limit on the project the key belongs to. It is the one control that caps what this or any later mistake can cost you.
  • Move the call behind an endpoint you own, and give that endpoint a rate limit of its own. A browser should never hold a key that spends money.
  • Read your usage page for the days the key was live. Rotation stops what happens next and says nothing about what already happened.
  • Keep a VITE_ or NEXT_PUBLIC_ prefix off anything you would mind a stranger reading. Those prefixes mean public, and your build tool takes them at their word.

If you would rather work through this in one sitting, the 10-minute security checklist covers this alongside the other things worth closing in a newly launched app. And for the wider question of which keys belong in a browser at all, we have a guide to telling publishable keys from secret ones and a census of what 30,998 apps actually shipped.

FAQ

Someone found my OpenAI key in my app. What is the first thing to do?

Rotate it. Open your OpenAI dashboard under API keys, create a new key, put the new one on your server, and delete the old one. Deleting it from your code is not the same thing, because the old value is still in your version history and in every cached copy of your page. After that, set a spend limit on the project and read your usage page for the days the key was live.

Can I restrict an OpenAI key to my own domain, the way I restrict a Google key?

No. A Google API key takes an HTTP referrer restriction that makes it work on your site and fail everywhere else, which is why a Google key in your page is usually fine. OpenAI offers nothing equivalent. There is no domain allowlist and no referrer check on an API key, so the only controls you have are on the account behind it: which project the key belongs to, what that project may spend, and whether the key still exists.

The OpenAI library has a dangerouslyAllowBrowser option. Does that make it safe?

No, and the name is the warning. OpenAI ships browser support switched off, and its own README says the option is dangerous because it exposes your secret API credentials in client-side code. Turning it on does not change what the browser can read; it only stops the library from refusing to start. The narrow cases it is meant for are internal tools with trusted users and short-lived development keys, not a public app.

How much can someone spend with a key they found?

As much as your account allows, which is why the spend limit matters more than the size of the bill you have seen so far. A stolen key draws on the same rate limits your app does, so the first symptom is often not the invoice at all: your own app starts failing while somebody else is busy. Set a monthly spend limit on the project and you have a ceiling on any of it.

I only used the key for a quick demo. Does it still matter?

Yes. A key stays live until somebody revokes it, and it does not know it was meant to be temporary. Automated scrapers collect key-shaped strings from public pages continuously, so the age of the demo works against you rather than for you. Deleting the key takes less time than deciding whether it was worth deleting.

Written by

Vlad Tkachenko

Founder, Reeve

I spend my time looking at apps built with Lovable, Bolt, v0, Cursor and Replit, and at the short list of mistakes that keep turning up in them.

More about the author

Read next

All articles

Not sure where your own app stands?

Run a free scan and get a plain-language grade from A to F in about 20 seconds. No account, no card.

Scan your app free

Automated external check, not a full audit. Absence of findings is not a guarantee of safety.