Thursday Thoughts: Audit Your Vibe Code, Often
Someone I know built a web app with Google AI Studio. React frontend, Firebase auth, Gemini API for the AI features, deployed on Vercel. A real product — users, a custom domain, the works. Built fast, shipped fast, worked great.
Then they got an email from Google: API key compromised, project suspended, locked out of Google Cloud Platform entirely. Couldn't access the console. Couldn't revoke the key. Couldn't see the billing. Couldn't fix anything.
The root cause took about thirty seconds to find. The Gemini API key was hardcoded in the client-side JavaScript bundle. Not in an environment variable on the server. Not behind a proxy. In the bundle. Minified, sure — but view-source doesn't care about minification.
Anyone who visited the site could open DevTools, search the bundle for AIza, and walk away with a working Gemini API key billed to someone else's account. Someone did. Google noticed the abuse, suspended the project, and now the developer is locked out of everything while they work through Google support to get it restored.
This isn't a story about one careless developer. It's a story about what happens when AI coding tools optimize for "make it work" and nobody in the pipeline checks for "make it safe."
How it got there
When you tell Google AI Studio — or any vibe coding tool — something like "build me a React app that uses Gemini to generate content," the model takes the shortest path to a working demo. That path is:
import { GoogleGenAI } from '@google/genai';
const client = new GoogleGenAI({ apiKey: "AIza..." });
const response = await client.models.generateContent({ ... });
Three lines. Works immediately. No backend, no proxy, no infrastructure. From the model's perspective, the task is done. The app calls Gemini and renders the result. Ship it.
The model doesn't think about what happens when those three lines end up in a production JavaScript bundle served to the public internet. It doesn't reason about deployment context. It doesn't have a threat model. It solved the functional requirement — "call Gemini, get a response" — and moved on.
Why the model gets this wrong
Five compounding factors:
The training data is full of tutorials that do exactly this. Every quickstart guide, every blog post, every "Getting Started with Gemini" doc shows the API key inline. Because they're teaching the API, not teaching production architecture. The model learned from thousands of examples where hardcoding the key was the correct thing to do — in a tutorial context.
Vibe coding collapses the frontend/backend boundary. A traditional app has a clear separation: API keys go on the server, the client talks to your server, your server talks to the external API. But when you prompt an AI to build a full app in one shot, that boundary doesn't exist unless you explicitly ask for it. The model generates a single-page React app. There's no server. The key goes where it works: the client.
LLMs don't reason about deployment. The model doesn't think "this code will be minified into a bundle, served via CDN, and visible to anyone with a browser." It generates code that satisfies the functional requirement in the current context. The concept of "this code is about to become public" isn't part of its reasoning.
No tool in the pipeline catches it. Google AI Studio generates the code. Vercel deploys it. Firebase serves the auth. None of them scan the build output for exposed API keys. None of them warn that a billable secret is shipping in client-side JavaScript. The developer pushes, Vercel builds, the site goes live, and the key is public.
The platform irony. Google AI Studio is building an app that calls Google's own API, and it doesn't flag that the key it's embedding will be publicly visible. Google knows which of its API keys are billable. They could build guardrails — warn when a Gemini key appears in client-side code, auto-generate a serverless proxy, or at minimum add a comment saying "move this server-side before deploying." They don't.
The blast radius
When a Gemini API key leaks in a client bundle, the damage escalates fast:
- Unauthorized API usage. Anyone with the key can make Gemini calls billed to the developer's account. Automated scrapers can burn through thousands of dollars in hours.
- Project suspension. Google detects the abuse pattern and suspends the GCP project. This is the right security response from Google's side — but it locks the developer out of everything, not just the compromised key.
- Cascading lockout. The suspension doesn't just affect Gemini. It hits Firebase, Cloud Storage, the console itself. If the billing account is shared across projects, other projects can be affected too.
- Recovery requires support. You can't self-service your way out of a suspended project. You need to contact Google Cloud support, explain what happened, dispute unauthorized charges, and wait.
The developer in this case is currently working through that process. They're not locked out because they did something malicious. They're locked out because the tool they used to build the app put a billable secret in a public place, and they didn't know to check for it.
The fix is architecture, not configuration
The correct architecture for any app that calls a paid external API from a web frontend:
Browser → Your backend (serverless function) → External API
↑ ↑
API key lives here Never touches client
For a Vercel-deployed app, that means:
- Create a serverless function (e.g.,
/api/generate) that holds the API key as a server-side environment variable set in the Vercel dashboard. - The frontend calls
/api/generatewith the user's prompt. - The serverless function calls Gemini with the key, returns the result.
- Add authentication checks — verify the Firebase ID token so only logged-in users can trigger API calls.
- Add rate limiting — cap requests per user per minute.
The API key never appears in any client-side code. It exists only in the Vercel environment, only accessible to your server-side functions.
On top of the architecture fix:
- Restrict the key in GCP Console → Credentials. Lock it to your serverless function's domain or IP.
- Set a daily quota cap on the Gemini API. Even if a key leaks again, the damage is bounded.
- Enable billing alerts at low thresholds — $10, $50, $100 — so you know before Google does.
Why we audit
This is exactly the class of bug that regular audits exist to catch.
I've written about our audit practice before — three agents auditing this blog, a spring cleaning of a year-old fitness tracker. In both cases, the most valuable findings were things the builder didn't know were wrong. A migration route accessible in production. API routes with no secondary auth checks. A scheduled-publish draft with leaked identifiers that would have gone live at 5 AM.
The pattern is the same every time: the person who built the app was optimizing for features, not for security. The app worked. The code was functional. And somewhere in the gap between "works" and "safe," a bug was waiting.
A Gemini API key in a JavaScript bundle is exactly this bug. It works. The app calls Gemini and renders the result. The developer tests it, it behaves correctly, they ship it. Nothing in the development workflow surfaces the fact that the key is now public. It hides in plain sight until someone finds it — either an auditor or an attacker.
The audits we run on this blog aren't paranoia. They're the acknowledgment that vibe coded apps — apps built fast, by AI, with the builder optimizing for "make it work" — accumulate a specific class of debt that only shows up when someone looks for it with fresh eyes. API keys in bundles. Auth checks that rely on a single middleware layer. Verbose error messages leaking stack traces. Security headers that were never added because the app worked without them.
If you're vibe coding — and at this point, most of us are — the question isn't whether your app has this kind of bug. It's whether you find it before someone else does.
What the tools should do
This is a solvable problem. Not by the developer — by the platforms.
Google AI Studio should detect when generated code embeds a Gemini API key in client-side JavaScript and either refuse, warn, or generate a server-side proxy automatically. Google has the context: they know the key is theirs, they know it's billable, and they know the code is going to a browser.
Vercel should scan build output for common API key patterns (AIza, sk-, AKIA) and flag them as warnings during deployment. They already analyze bundles for size — analyzing them for secrets is the same capability.
Firebase should enforce App Check by default on new projects, not as an opt-in feature that most developers don't know exists.
Every AI coding tool should treat "API key in client-side code" as a lint error, not a feature. The same way ESLint flags unused variables, these tools should flag exposed secrets. The pattern is well-known. The regex is simple. The cost of not catching it is real.
Until the tools catch up, the defense is audits. Regular, systematic, fresh-eyes reviews of what your app actually ships to the browser. Not what you think it ships. What it actually ships.
By the Numbers
- 1 API key in a client-side JavaScript bundle — all it takes
- 30 seconds to find it with DevTools and a search for
AIza - 0 warnings from Google AI Studio, Vercel, or Firebase during the entire build-and-deploy pipeline
- 3 lines of code that the AI generated to make it "work" — and created the vulnerability
- 100% of the damage preventable with a serverless proxy function
- 0 vibe coding tools that currently scan for API keys in client bundles
- 1 regular audit — the difference between finding it yourself and getting the email from Google