VibeCPR

← Blog

Secrets · Survival guide

How to find and fix leaked API keys in a Lovable, Bolt or Cursor app

Published · VibeCPR

TL;DR If a secret key (Stripe sk_live_, OpenAI sk-, Supabase service_role or sb_secret_) is in your frontend code or git history, assume it's compromised. Rotate it first, then move the call to a server or edge function, keep the new key in a server-only environment variable, and add a secret scanner so it can't happen again.

Why AI-built apps leak keys

AI app builders optimize for “it works.” The fastest way to make a Stripe or OpenAI call work is to paste the key right where the call happens, often in a React component that ships to every visitor’s browser.

Frameworks make this easy to do by accident. Vite’s docs say variables prefixed with VITE_ “will be exposed in client-side source code after Vite bundling” and “should not contain sensitive information such as API keys” (Vite docs). Next.js does the same for anything prefixed NEXT_PUBLIC_ (Next.js docs). Rename STRIPE_SECRET_KEY to VITE_STRIPE_SECRET_KEY to “fix” an undefined error, and you’ve published it.

The cost is real. One founder’s AI-built app exposed a live Stripe secret key; 175 customers were charged $500 each and he lost about $2,500 in Stripe fees. Escape’s research counted 400+ leaked secrets across 1.4K vibe-coded apps.

Which keys are safe in the browser and which aren’t?

KeySafe in frontend?Notes
Stripe publishable pk_live_…YesDesigned to be public
Stripe secret sk_live_… / restricted rk_live_…NoServer only
Supabase anon / sb_publishable_…Yes, if RLS is on for every tableThe public key only reaches what RLS allows
Supabase service_role / sb_secret_…NeverBypasses all Row Level Security
OpenAI / Anthropic / Gemini keysNoAnyone can spend your credits
Resend, SendGrid, Twilio keysNoCan send email/SMS as you

Supabase’s API keys docs are blunt about the secret key: “A secret key bypasses every Row Level Security policy you have. Never put one in a browser, a shipped application, or source control.”

Tip: a legacy Supabase key is a JWT starting with eyJ. Paste it into a JWT decoder and look at the role field: anon is the public key; service_role is the master key.

Step 1: Find leaked keys in your live app (5 minutes, no tools)

  1. Open your live site in Chrome, press F12, go to Sources (or Network → JS).
  2. Press Ctrl+Shift+F (Cmd+Option+F on Mac) to search all loaded files.
  3. Search for each of: sk_live, rk_live, sk-, service_role, sb_secret, eyJ, api_key, apiKey, secret.
  4. For each hit, ask: is this a key from the “No” column above? If yes, it’s leaked.

Also try opening https://your-app.com/.env and https://your-app.com/.git/config. They should return 404.

Step 2: Find leaked keys in your code and git history

Deleting a key from your code doesn’t remove it from git history. Anyone who can see the repo can still find it. Use gitleaks, a free open-source scanner:

# scan the current files
gitleaks dir . -v
# scan every commit ever made
gitleaks git . -v

If your repo is public on GitHub, treat anything found in history as already leaked.

Step 3: Rotate the key first, then clean up

Order matters. GitHub’s own guidance says that for a leaked secret, “as a first step you need to revoke and/or rotate that secret” (GitHub Docs).

  • Stripe: Dashboard → Developers → API keys → roll the secret key. Then check Payments and Customers for activity you don’t recognize.
  • OpenAI / Anthropic: delete the key in the platform console, create a new one, and set a monthly spend limit.
  • Supabase: rotate or replace the secret key in Project Settings → API keys, then review your tables for unexpected rows.

Then check your provider’s logs for the window when the key was exposed.

Step 4: Move the call to the server

The new key should live only where visitors can’t see it:

  • Lovable: add the key under Cloud → Secrets and call the provider from an Edge Function, not from a React component. Lovable’s docs say secrets are stored so “only your app can use them” (Lovable Cloud docs).
  • Next.js (Cursor, v0, Claude Code): keep the key in an env var without NEXT_PUBLIC_, and call the provider from a Route Handler or Server Action.
  • Vite / Bolt: keep the key without the VITE_ prefix and call it from a serverless function (Netlify/Vercel functions or a Supabase Edge Function).

Copy-paste prompt for your AI tool:

Security fix: the [Stripe/OpenAI/...] secret key must never reach the browser.
1. Remove every use of the key from client components and any VITE_ / NEXT_PUBLIC_ variable.
2. Create a server-side endpoint (edge function / route handler) that makes the API call,
   reads the key from a server-only env var, and checks the user is logged in.
3. Update the frontend to call that endpoint instead.
4. Add .env* to .gitignore. Show me the diff before applying.

Step 5: Make sure it never happens again

  • Run gitleaks as a pre-commit hook or in CI.
  • Turn on GitHub secret scanning and push protection for your repo.
  • Set spending limits and usage alerts on every paid API.
  • Add one line to your AI tool’s rules: “Never put secret keys in client code or public env variables.”
  • Re-scan after every big AI-generated change.

FAQ

I deleted the key from my code. Am I safe? No. It’s still in your git history and in any build already deployed. Rotate it.

Is my Supabase anon key a leak? No, it’s designed to be public. The real risk is tables without Row Level Security. See our Supabase October 30 guide.

How do I know if someone used my leaked key? Check the provider’s dashboard logs (Stripe events, OpenAI usage, Supabase logs) for the period it was exposed.