How to find and fix leaked API keys in a Lovable, Bolt or Cursor app
Published · VibeCPR
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?
| Key | Safe in frontend? | Notes |
|---|---|---|
Stripe publishable pk_live_… | Yes | Designed to be public |
Stripe secret sk_live_… / restricted rk_live_… | No | Server only |
Supabase anon / sb_publishable_… | Yes, if RLS is on for every table | The public key only reaches what RLS allows |
Supabase service_role / sb_secret_… | Never | Bypasses all Row Level Security |
| OpenAI / Anthropic / Gemini keys | No | Anyone can spend your credits |
| Resend, SendGrid, Twilio keys | No | Can 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)
- Open your live site in Chrome, press F12, go to Sources (or Network → JS).
- Press Ctrl+Shift+F (Cmd+Option+F on Mac) to search all loaded files.
- Search for each of:
sk_live,rk_live,sk-,service_role,sb_secret,eyJ,api_key,apiKey,secret. - 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
gitleaksas 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.
Get Patch Notes
One email a week: breaking changes and security fixes for apps built with AI. Also puts you on the waitlist for the free Vibe Check.