How to find and fix exposed API keys in your AI-built app
Not every key visible in your JavaScript is a breach. Here is exactly how to tell the difference, before you panic-rotate everything or ignore something critical.
The rule that actually matters
Some API keys are built to be public — they identify your project, not authorize privileged actions, and providers design their systems assuming anyone can see them. Others grant real power: spending money, reading private data, deleting resources. Finding a key in your bundle tells you nothing on its own — which category it falls into tells you everything.
- Stripe Publishable Key
pk_live_... - Firebase Web Config API Key
apiKey: "..." - Supabase Anon / Publishable Key
eyJhbGci...
- OpenAI API Key
sk-... - Anthropic API Key
sk-ant-... - Stripe Secret Key
sk_live_... - Google API Key
AIza... - GitHub Token
ghp_... / gho_... - AWS Access Key
AKIA... - Supabase Service Role Key
service_role...eyJ...
Where AI coding tools leak these
The most common pattern: asking your AI coding tool to "connect to OpenAI" or "add Stripe checkout" and it wires the secret key directly into a client component or a fetch call that runs in the browser, because that is the shortest path to something visibly working. It is correct code in the sense that it runs — it is wrong in the sense that anyone who opens devtools now has your key.
The fix
Move the call server-side. In Next.js, that means a Route Handler reading the key from an environment variable that is never prefixed NEXT_PUBLIC_:
// app/api/chat/route.ts — runs server-side, key never reaches the browser
export async function POST(req: Request) {
const { prompt } = await req.json();
const res = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`, // server-only env var
'Content-Type': 'application/json',
},
body: JSON.stringify({ /* ... */ }),
});
return Response.json(await res.json());
}Your frontend then calls /api/chat on your own domain — the browser never sees the real key. Then rotate the exposed key with the provider immediately; the old one should be treated as fully compromised the moment it was visible.
Want to know exactly which keys in your app are actually dangerous?
StackSecured scans your live bundles and tells you which findings are real critical exposures versus safe-by-design public keys — no guessing.
Run a free scanCommon questions
I found a key starting with "eyJ" in my bundle — is that a leak?
+
It depends which one. A Supabase anon/publishable key also starts with "eyJ" (it is a JWT) and is safe to expose — it is designed for client-side use. A Supabase service_role key is a different, far more powerful JWT that must never leave your server; it bypasses Row Level Security entirely. Check the surrounding context in your code — a key referenced as SUPABASE_SERVICE_ROLE_KEY or similar in client-side code is a critical, not a false positive.
Why does my AI coding tool put secret keys in client-side code in the first place?
+
Cursor, Bolt, Lovable, and v0 generate whatever gets a feature working fastest. If you ask for "call the OpenAI API from my app," the fastest working path is often a client-side fetch call with the key inline — it runs, it demos well, and nothing in the tool flags that the key is now visible to every visitor. The fix is architectural (move the call to a server route/API endpoint), not something the AI tool decides on its own to do correctly by default.
I rotated a leaked key — am I done?
+
Rotating stops future abuse but does not undo anything that already happened with the old key. Check your provider's usage/billing dashboard for the affected key's activity during the exposure window for spend or requests you do not recognize. For OpenAI, Stripe, and AWS specifically, also check for any new API keys, users, or resources created during that window that you did not create yourself.
How do I stop this from happening again?
+
Any key with real privileges — one that can spend money, delete data, or access private records — belongs in a server-side environment variable, referenced only from server-side code (an API route, an Edge Function, a backend service). It should never appear in anything shipped to the browser: not in a script tag, not in a client component, not in a public repo's committed .env file.
More guides: Supabase RLS: the #1 vibe-coding bug · Full vibe-coding security checklist