← Blog

How to find the API keys your vibe-coded app is leaking

Exposed secrets are the single most expensive mistake an AI-built app ships with — and one of the easiest to check. A leaked Stripe key, database URL or third-party token sitting in your frontend is readable by anyone who opens your site, and once it’s out, the only real fix is to rotate it. Here’s how keys end up exposed, how to check yours in two minutes, and what to do the moment you find one.

Public by design vs. secret by necessity

Some keys are meant to ship to the browser. Supabase’s anon key, Stripe’s publishable key (pk_...), a Firebase web config, a Google Maps browser key — these are designed to be public and are gated by other controls. The danger is the other kind: anything labelled secret, service, private, or starting sk_ — Stripe secret keys, database connection strings, service-role keys, API tokens for email, SMS or LLM providers. Those grant real power and must never reach the client.

The bug is simple: a secret key ends up somewhere the browser can read it.

The four ways they leak

  1. The NEXT_PUBLIC_ / VITE_ / PUBLIC_ prefix. Frontend frameworks inline any env var with the public prefix straight into the browser bundle. Put a secret behind NEXT_PUBLIC_STRIPE_SECRET and it’s public — the prefix is a promise to ship it.
  2. Imported into a client component. Even without the prefix, any value imported — even transitively — into a "use client" component or a frontend file gets bundled. The AI doesn’t always keep the server/client line clean.
  3. Hardcoded while iterating. “Just paste the key in for now” during a fast build, never moved out.
  4. Committed to git. A real .env checked in once stays in the git history forever, even after you delete the file — anyone who ever had repo access can still read it.

Check yours in two minutes

  • Read your own bundle. Open your live site, View Source, and search the page and its JavaScript for sk_, secret, service_role, password, Bearer. Or open DevTools → Sources and search across files. If a secret is in there, so is everyone else’s copy.
  • Grep the repo — and the history. git grep -iE "secret|sk_live|service_role|password" finds current ones; git log -p | grep -iE "sk_live|service_role" catches anything committed and later removed.
  • Watch the Network tab. Load the app with DevTools open and look at request payloads and headers. A secret riding along in a request the browser makes is a secret the browser knows.

What to do when you find one

  1. Rotate it first. Assume it’s compromised — generate a new key in the provider’s dashboard and revoke the old one. Moving it somewhere safer without rotating leaves the leaked value live.
  2. Move it server-side. The new secret lives only in a server environment variable, used in API routes, server functions or edge functions — never imported into client code.
  3. If it was in git, rotation is the fix. Rewriting history is good hygiene, but the key is already out; rotating is what actually closes it.
  4. Re-check after deploying. Rebuild, reload, and grep the bundle again to confirm it’s gone for real.

Exposed keys are the most common critical finding we see, and among the most boring to fix once you know where to look. Our free 2-minute self-check asks about exactly this, and a seniorgrade audit reads your actual code and bundle to tell you what’s exposed — including the keys a quick grep misses. On a specific stack, the Next.js and Supabase breakdowns cover how secrets leak there in particular.