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.

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.
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.
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:
| What the scan looks at | The question it answers |
|---|---|
| Secret keys in your code | Is a paid-API or admin key readable by anyone who looks? |
| Database rules | Can a stranger read your users' rows without logging in? |
| Private files | Are .env files or database dumps downloadable from a URL? |
| Security headers | Are the browser-side protections switched on? |
| Storage buckets | Can anyone list the files your users uploaded? |
| Source maps | Is your original source code published alongside the app? |
| Open APIs and CORS | Do your endpoints answer any website that asks? |
| Certificate expiry | Is HTTPS valid, and not about to lapse on your visitors? |
| Domain renewal | Is 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_orNEXT_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.