Key takeaway: AI coding tools default to calling OpenAI, Stripe, and Supabase from the browser, reading secrets from NEXT_PUBLIC_ or VITE_ variables that get baked into the client bundle. The app demos fine, but the keys ship to every visitor. Grep your production build for sk-, sk_live, and service_role; if you find one, rotate it before you refactor.
Open the devtools on a fast-built MVP and the first thing worth looking at isn't the UI. It's the JavaScript bundle.
That's where the keys usually are.
Not always. Not on every project. But often enough that it's the single most common finding when we read AI-generated codebases. An OpenAI key that starts with sk-, a Stripe key that starts with pk_live or worse sk_live, a Supabase service_role token sitting in a config file the build step happily inlines into the frontend. The app works in the demo because the keys work. They also work for anyone else who opens the network tab.
How it gets there
An AI tool wires the call to run in the browser and reads the secret from a NEXT_PUBLIC_ or VITE_ variable, which the framework bakes into the client bundle at build time.
The pattern is almost always the same, and it isn't a sign of bad work. The founder asks an AI tool to wire up an OpenAI call from the chat box, or a Stripe checkout from the pricing page, or a Supabase query from the dashboard. The tool generates code that runs in the browser, because that's the simplest thing that compiles and demos. It reads a key from an environment variable named something like NEXT_PUBLIC_OPENAI_API_KEY or VITE_STRIPE_KEY.
That NEXT_PUBLIC_ or VITE_ prefix is the tell. Those prefixes mean the value gets baked into the client bundle at build time. The framework is doing exactly what it was asked. The tool generating the code didn't stop to ask whether this particular value should ever leave the server.
The demo works. The founder ships it. Nobody opens devtools because nobody's looking for trouble yet.
What it costs when someone does look
A leaked OpenAI key burns your budget, a leaked Stripe secret key lets someone charge and refund at will, and a leaked Supabase service_role key hands over every row in the database.
A leaked OpenAI key is somebody else's budget to play with. Until you notice, it's running their experiments on your invoice. We've seen this turn into a four-figure overnight bill more than once, and the founder only finds out from the email from the provider, not from their own monitoring.
A leaked Stripe publishable key is mostly fine, that's what it's for. A leaked Stripe secret key is a different category entirely. With it, somebody can refund themselves, create charges, pull customer data. Same shape of mistake, very different blast radius.
A leaked Supabase service_role key is the worst of the three for a multi-tenant app. The service_role bypasses row-level security by design. Anyone holding it can read and write any row in any table, regardless of which user they're logged in as. If your tenant isolation depends on RLS, and the service key is in the browser, your tenant isolation is decorative.
Why the audit catches it and the demo doesn't
The demo only runs the happy path, so nobody opens devtools or reads the bundle; an audit reads for exactly the adversarial cases the demo never touches.
The demo only ever exercises the happy path: one developer, one account, one well-behaved click through the flow. Nobody opens devtools, nobody reads the bundle, nobody points a script at the endpoint. Every adversarial behaviour the code needs to survive in production is absent from the test that proved the code worked.
Reading for what the happy path never touches is most of what an audit actually is. The keys check is the cheapest part of that read, and it catches some of the most expensive problems.
What to do today, before anyone reads this for you
Search your repo and your built bundle for the key patterns, and if you find one, rotate it at the provider before you move the call server-side.
Three quick things, in order of how fast you can do them.
- Search your repo for the patterns. The strings sk-, sk_live, pk_live, service_role, and the prefix SUPABASE_SERVICE in any file that ships to the browser. On most stacks that means any file under a client, src, or app folder that isn't explicitly server-only. If any of those hit, you have work to do.
- Grep your built bundle. Run a production build and search the output for the same strings. The bundle is what users actually receive, and it's the ground truth for what's exposed. A key that's only referenced in a .env.example is fine. A key that ends up in main.js is not.
- If you find one, rotate before you refactor. The key is already public the moment it shipped. Rotating it at the provider invalidates the leaked copy immediately. Then you can move the call server-side at your own pace, behind an endpoint your app owns, with auth on the request and a spend ceiling on the provider account.
The pattern under the pattern
AI tools optimise for code that runs, not code that runs safely under attack, so someone has to be asked to look for the exposure on purpose.
None of this is exotic, and none of it is the founder's fault for using AI tools. The tools optimise for code that runs. Code that runs safely under adversarial conditions is a separate problem that needs somebody, human or otherwise, who was asked to look for it.
If you've built an MVP fast and you're about to put real users on it, or hand it to your first enterprise customer, the keys are the first thing worth checking. It's a small read with a big blast radius if it goes wrong. That read is one of the first things we do in an audit, and it's usually one of the first things we find.
Frequently Asked Questions
How do API keys end up in a frontend bundle?
Usually through an environment variable prefixed with NEXT_PUBLIC_ or VITE_. Those prefixes tell the framework to inline the value into the client bundle at build time, so a key meant for the server ships to every visitor's browser.
How do I check whether my app exposes API keys?
Run a production build and grep the output for strings like sk-, sk_live, pk_live, and service_role. The built bundle is what users actually receive, so it is the ground truth for what is exposed.
What should I do if I find a leaked key?
Rotate it at the provider first, since it is already public the moment it shipped, and rotating invalidates the leaked copy immediately. Then move the call server-side behind an endpoint you own, with auth on the request and a spend ceiling.
Related Services
Need help with what you just read? These services are directly relevant.
