VibeSecurity

Incidents and lessons

Exposed API Keys in Vibe-Coded Apps: Leaks and Rotation

Exposed API keys are the most preventable way for an AI-built app to lose money or data. A key pasted into a chat, prefixed with the wrong variable name, or committed once to a repository is enough. This post covers how keys leak, which ones are designed to be public, the exact rotation order that avoids both downtime and a second leak, and how to limit the damage before anything goes wrong.

By the VibeSecurity team6 min read

How do keys leak from AI-built apps?

  • Client bundles. A provider call written directly in a React component ships the key to every visitor. Anyone can read it in developer tools.
  • Public environment prefixes. Next.js inlines NEXT_PUBLIC_ variables into the JavaScript sent to the browser at build time, and Vite exposes VITE_ variables to client code. Vite's docs say such variables should not contain sensitive information like API keys.
  • Repositories. A .env file committed once stays in history, and a private repository can become public later.
  • Chat pastes. Sharing a key with an assistant, a teammate or a support thread copies it to systems you do not control. Supabase's docs say secret keys should never be transmitted via chat, email or URLs.
  • Source maps. Publishing them with production exposes original source, including hard-coded values.
  • Logs. Request logging that dumps headers, error reporters that capture environment variables, and screenshots of terminals all preserve keys in places with wide access.

Why does the prefix matter so much?

Frameworks are consistent here. In Next.js, variables without the NEXT_PUBLIC_ prefix are only available in the Node.js environment. Adding the prefix tells the build to replace every reference with the literal value, and the value freezes at build time. Rename an existing variable to make a component work, and you have just published it.

A useful rule for review: search for every use of a public prefix and read the variable name aloud. If it contains SECRET, SERVICE, PRIVATE or the name of a paid provider's server key, stop.

Which keys are safe to expose, and which are not?

Safety is a property of what the key can do, not of how it looks. A publishable key is one the provider designed for browsers, and its power is deliberately limited. A secret key acts with your account's authority.

Exposure by key type. Confirm each in your provider's current docs before relying on it.
Key typeExampleSafe in browser?Why
Supabase publishable or anon keyProject URL plus public keyYesDesigned to be exposed. Access is limited by Row Level Security.
Supabase secret or service_role keyServer-side onlyNoBypasses RLS, so it grants full access to the project's data.
Stripe publishable keypk_live_ or pk_test_ prefixYesCannot perform sensitive operations such as creating charges or reading account data.
Stripe secret or restricted keysk_ or rk_ prefixNoSecret keys have unrestricted permissions. Restricted keys are limited but still sensitive.
LLM provider API keyProvider dashboard keyNoAnyone holding it can spend your budget. Call the provider from a server route.
Database connection stringpostgres:// URLNoContains credentials with direct access to your data.
Webhook signing secretPer-endpoint secretNoLets someone forge events your server would trust. Stripe treats these as separate from API keys.

How do I check my own app for leaked keys?

Work on your own deployment and repository. Open the live app, use developer tools to search all loaded sources, and search the repository including history. Look for provider prefixes and connection strings. If you find something and are unsure whether it is secret, check the provider's documentation for that key type.

Search your own repo, including history
git grep -nE "sk_live_|sk_test_|service_role|BEGIN (RSA |EC )?PRIVATE KEY|postgres://|mongodb\+srv://" $(git rev-list --all)

The rotation runbook, in the right order

Order matters. Revoke first and your app goes down. Create a replacement and forget to revoke, and the attacker keeps access. This sequence avoids both.

  1. 1Contain the source. Remove the key from the public repo, page or paste if you can, but do not treat removal as the fix. The value is already copied.
  2. 2Create a replacement key in the provider dashboard. Prefer a restricted or scoped one that grants only what the app needs.
  3. 3Store it in your hosting platform's secrets store or a server-side environment variable, never in a client-exposed one.
  4. 4Deploy the app using the new key and confirm the main flows work. Stripe suggests rolling out gradually and watching logs for errors before retiring the old key.
  5. 5Revoke or expire the old key. Stripe's Dashboard rotation can leave the old key valid for a grace period, so choose an expiry of Now if the old key is known to be compromised.
  6. 6Check the provider's request logs and usage for activity you do not recognize, from before the revocation.
  7. 7Review downstream damage: unexpected spend, data reads, created resources, forwarded emails. Escalate to the provider if you see misuse.
  8. 8Clean history afterwards with a tool such as git filter-repo. GitHub notes that git revert does not remove sensitive data because the original commit stays in history. Rewriting is hygiene; revocation is the fix.

How do I limit the blast radius before a leak?

  • Use scoped keys. Stripe recommends restricted keys for most server-side use, so a stolen key can only do what you granted.
  • Use separate keys per component and environment. Supabase advises separate secret keys per backend component so a breach stays contained.
  • Set spend controls. OpenAI's documentation describes spend alerts and hard spend limits to prevent unexpected costs from a compromised key.
  • Add access policies where offered. Stripe lets you restrict a key to known IP addresses or networks and notifies you of blocked requests.
  • Turn on usage tracking and alerts so unusual traffic reaches you within hours, not at the end of the month.
  • Set maximum lifetimes for keys where the provider supports it, so forgotten keys expire.

A realistic scenario

A founder asks an assistant to add an AI summary feature. The model writes a fetch call in a component and, to make it run, reads a variable named VITE_LLM_KEY. The demo works. The site deploys. The key now sits in a JavaScript file that any visitor can download.

The fix is structural, not cosmetic. Create a server route that holds the key, takes a validated request from the browser, calls the provider and returns only the result. Then rotate the exposed key, cap spending, and confirm the new key appears nowhere in the built output.

Frequently asked questions

Can I just delete an exposed key from my code?

No. The old value stays valid until you revoke it, and it may already be copied from history, caches or a public repo. Create a replacement, deploy it, then revoke the old key. Removing it from code and cleaning history is hygiene after revocation, not a substitute.

Is a Stripe publishable key okay to expose?

Yes. Stripe documents publishable keys as safe to include in front-end code and mobile apps because they cannot perform sensitive operations such as creating charges or reading account data. Secret keys and restricted keys must stay on a server.

How do I stop an assistant from putting keys in my frontend?

Put explicit rules in your project instructions: secrets only in server-side code, no public-prefix variables for private keys, and provider calls through a server route. Then verify by searching the built bundle, since rules reduce mistakes but do not guarantee compliance.

What should I do first when I find a leaked key?

Create a replacement key, deploy it, and revoke the old key. Then check provider logs for unfamiliar activity and set a spend cap. Speed matters, but the order prevents an outage while still ensuring the exposed key stops working.

Put it into practice

Sources

  1. 1.Supabase docs: API keys
  2. 2.Stripe docs: API keys
  3. 3.Next.js docs: Environment variables
  4. 4.Vite docs: Env variables and modes
  5. 5.OpenAI docs: Production best practices
  6. 6.GitHub docs: About secret scanning