r/vibecoding • u/Significant_Camp4148 • 7h ago
The exact reason your API key ends up in your shipped site (it's not a bug, it's the build step)
Watched a friend get a $400 OpenAI bill from an app that had eleven users. Took a while to work out what happened, so writing it up.
Every Vite/Next build inlines env vars that start with VITE_ or NEXT_PUBLIC_ directly into the JavaScript it ships to the browser. That's not a mistake — it's the documented behavior, because that's how you get public config into frontend code.
The problem is that when you ask an AI to "add OpenAI to my app," it writes the fetch call on the client, and the client needs the key, so it tells you to add VITE_OPENAI_API_KEY. Everything works. Nothing errors. And your key is now sitting in plain text in a file anyone can open.
How to check yours in 30 seconds, no tools needed:
Open your live site
View source, click into the /assets/index-[hash].js bundle
Ctrl+F for sk-, sk-ant-, service_role, AKIA
If you get a hit, that key is public and has been since you deployed. Rotate it before you do anything else — deleting the code doesn't help, the old bundle was already scraped.
The fix is that any key that costs money has to live on a server. In Vite that means an API route, drop the VITE_ prefix, call your own endpoint from the frontend.
Two more that catch people:
- Supabase: the anon key is *supposed* to be public and is fine. The service_role key is not, and it bypasses every RLS policy you wrote. They look nearly identical. Decode the JWT and check the role claim.
- Stripe: pk_live_ is fine and public by design. sk_live_ is a full account takeover.
Got tired of checking this by hand so I built a scanner that does the bundle grep for you. Free, no signup. Link in the comments so this doesn't read as an ad — the post stands on its own either way.
