API Key Exposed in Client Bundle: React
API keys in React bundles are visible to attackers. Learn how to detect exposure, rotate keys immediately, purge git history, and prevent this with backend proxies.
- API keys prefixed with REACT_APP_, NEXT_PUBLIC_, or VITE_ are bundled into your browser JavaScript and visible to attackers
- Automated bots scan GitHub and Vercel deploys for Stripe (sk_live_), AWS (AKIA), and Supabase (sb_secret_) keys; exposed keys are found within days
- Fix immediately: rotate the key in your provider dashboard, purge git history with git-filter-repo, then redeploy without the secret
- Prevent permanently: use a backend proxy so the client never sees the key, or use Next.js Server Components which keep secrets server-only
- Pre-commit hooks with trufflehog catch exposed secrets before they reach git history
API keys in your React client bundle are visible to anyone who inspects your code in DevTools or reads your deployed JavaScript. This is dangerous, and it happens in most AI-generated apps. Here is how it happens, why it matters, how to find it, how to fix it if you have it, and how to prevent it forever.
What Happens When an API Key Lands in Your Client Bundle?
An API key ends up in your client-side JavaScript when you import it into a React component. Build tools inline environment variables prefixed with REACT_APP_, NEXT_PUBLIC_, or VITE_ directly into the bundled JavaScript file. When the browser downloads your app, that secret is plain text, visible to anyone who opens the Sources tab in DevTools.
AI-generated apps are especially vulnerable. Tools like Lovable, Bolt, v0, and Cursor optimize for speed over security. When you ask for "connect to OpenAI," they generate the shortest path: import the SDK in a client component and call it directly. The feature works immediately. You deploy. Nobody notices until someone opens DevTools and copies the key.
The naming convention should warn you. NEXT_PUBLIC_ means "this will be public." Many developers miss this and treat it as secret by default. The build step replaces the variable name with the actual value. If your.env has REACT_APP_OPENAI_KEY=sk-proj-123..., the browser receives the literal string sk-proj-123... embedded in the JavaScript bundle.
How Serious Is the Exposure?
A compromised API key is a direct attack surface. An attacker with your Stripe live key can charge your customers. An OpenAI key funds their compute instantly. An AWS key with broad permissions can delete your entire infrastructure.
Attackers automate this. Bots scan GitHub and deployed sites on Vercel and Netlify constantly, looking for patterns: sk_live_ (Stripe), AKIA (AWS), sb_secret_ (Supabase), ghp_ (GitHub). If your key matches, it will be found within days.
Real-world costs are severe. A leaked Stripe key costs hundreds of dollars in fraudulent charges within hours. A leaked database password means data breach and customer notification. The Resend email provider had API keys exposed in client code, leading to account compromise across customers.
The severity depends on key type. A public, read-only key is lower risk than a live billing key. But even a "public" key exposes your application logic and rate limits. The only safe key in the browser is no key.
How Do You Detect Exposure?
A key is exposed somewhere right now if it has ever been in your codebase. Find it fast.
Manual inspection: Open your deployed site, press F12 (or Cmd+Option+I on Mac), go to the Sources tab, and search for API key patterns. Look for sk_live_, AKIA, sb_secret_, or token-like strings. Expand your JavaScript files and search Ctrl+F for "apiKey", "token", "secret", or provider names.
Check your.env file. Any variable with REACT_APP_, NEXT_PUBLIC_, or VITE_ prefix will end up in the bundle. Variables without these prefixes might stay local, but verify per framework.
Automated scanning: Use trufflehog, an open-source scanner with 27.6K GitHub stars. It detects known secret patterns in your codebase and git history. Install it and run: trufflehog filesystem. --json. It returns high-confidence matches instantly.
Gitleaks is an alternative, specialized for scanning git repositories. Both are faster and more accurate than manual inspection.
For already-deployed bundles, cremit.io and unbreachable.dev offer scanning services as well.
What's the Immediate Fix if It's Already Exposed?
If you find your key in the bundle, act immediately in this order. Skipping steps leaves you exposed.
Step 1: Revoke the key immediately. Log into your provider -- Stripe, OpenAI, Supabase, AWS -- and regenerate or delete the key. In Stripe, go to Settings > API Keys > Rotate Secret Keys. In OpenAI, go to Account > API Keys > Delete. This stops new attacks. Do not delay or wait for billing alerts.
Step 2: Purge git history. Deleting the key from your code is not enough. Git keeps a permanent record in every commit. Attackers can access old commits and extract the secret. Use git-filter-repo (13.2K GitHub stars), the modern replacement for BFG Repo-Cleaner.
Create a file replacements.txt:
sk-oldkey123==>sk-newkey456
Then run:
git filter-repo --replace-text replacements.txt
Force-push to your main branch. GitHub, GitLab, and Bitbucket will warn you -- confirm the push. This rewrites every commit and deletes the old key from the entire history.
Step 3: Re-deploy without the key. Add the key to.env.local or.env.production.local (in.gitignore). Deploy immediately. The new bundle will not contain the key.
Rotation is the only complete fix. Hiding the key in old commits does not work. Hoping nobody found it is not a strategy.
What's the Right Architecture to Prevent This?
Secure API calls use a backend proxy. Your React component calls your own backend endpoint, like POST /api/generate. Your backend holds the real API key and makes the actual call to OpenAI or Stripe. The client never sees the secret.
Example: Instead of calling OpenAI directly from React with credentials, you call your backend:
fetch('/api/generate', {
method: 'POST',
body: JSON.stringify({ prompt })
})
Your backend routes to the real API using the secret stored server-side only. No secret ever touches the browser.
Managed hosting platforms like Ship enforce this architecture by design. The infrastructure separates client code from backend routes, making it structurally impossible to bundle secrets into the browser. You define API routes, and the client accesses them via HTTP only. Secrets stay on the server.
Next.js Server Components: Next.js 13+ supports Server Components, which run only on the server. Import your SDK and use the real key -- the code never reaches the browser. Build your data layer in Server Components, pass results to Client Components. This is the modern Next.js best practice and is more secure than Remix or tRPC patterns.
Environment scoping: In Next.js, variables without NEXT_PUBLIC_ stay server-only. In Vite, unprefixed variables stay private. In Create React App, anything without REACT_APP_ stays local. Know your framework's rules. Many breaches happen because developers didn't know which prefix was unsafe.
Why Do Vibe-Coders Generate Exposed Keys?
Lovable, v0, Bolt, Replit, and Base44 generate code optimized for speed, not production architecture. When you ask "connect to OpenAI," they produce the shortest working path: import the SDK, call it from a client component. The result works immediately.
This is not carelessness. It is economics. A production-ready backend proxy adds scaffolding the user must understand and debug. A direct API call works now. The tool gets credit for fast results.
When you export code from these tools, the JavaScript still includes the API calls as written. If you used a real key during testing, the export might contain it. You must manually refactor to a backend proxy before production. Many people do not realize this step is required.
Treat all AI-generated code as a prototype, not production-ready. Human-written prototypes usually get reviewed and refactored before deploy. AI prototypes often ship as-is.
How Do You Prevent Future Exposure?
Prevent exposure by automating detection and enforcing rules.
Pre-commit hooks: Install trufflehog as a pre-commit hook. Every commit is scanned for secrets. If found, the commit fails. This catches mistakes before they reach git.
CI/CD scanning: Add trufflehog to your GitHub Actions or GitLab CI pipeline. Scan every merge request before deploy. Make it a required check -- no deploy if secrets are found.
Code review checklist: Train your team on unsafe patterns:
- No NEXT_PUBLIC_ or REACT_APP_ on actual secrets
- No hardcoded API strings in client components
- No.env files in git (use.gitignore)
- All client API calls route through a backend proxy
- No database passwords imported into frontend code
Database and auth keys: Same rule applies to database passwords, session tokens, and third-party auth keys. Never import them in the browser. For Supabase, use the anonymous key (scoped, public) for client access, never the service role key.
The goal is to make secret management invisible. Set it up once, and it stays fixed.
Frequently Asked Questions
Is a NEXT_PUBLIC_ environment variable safe?
No. NEXT_PUBLIC_ bundles the value into the browser JavaScript. Do not use it for secrets. Use it only for public values like Sentry DSN or analytics keys that are meant to be exposed.
How do I know if my API key is already exposed?
Run trufflehog filesystem. --json on your repo. Open DevTools Sources, search for "sk_live_", "AKIA", or your provider name. Check.env for keys with public prefixes.
Should I regenerate or rotate my API key?
Regenerate means creating a new key and deleting the old one immediately. Rotate means creating a new key and phasing out the old one over time (if your provider allows). For exposed keys, regenerate immediately in your provider dashboard.
Why do Lovable/v0/Bolt generate code that exposes keys?
They optimize for time-to-demo, not production. Direct API calls work fastest. Treat their output as a prototype. Always refactor to a backend proxy before deploying to production.
Does deleting the key from my code fix it?
No. If it is in git history, attackers can still pull it from old commits. You must purge history with git-filter-repo, then rotate the key in your provider dashboard.
Can managed hosting platforms prevent this?
Yes. Platforms like Ship enforce backend routing structurally, making it impossible to accidentally bundle secrets into the browser. You configure API routes server-side, and the client accesses them as HTTP endpoints only.
The Bottom Line
API key exposure in client bundles is common, costly, and preventable. Fix it with immediate key rotation and architecture change. Prevent future leaks with pre-commit scanning and code review discipline.
If you are building on a managed platform like Ship, this is handled structurally. If you self-host, treat backend routing as non-negotiable. Get it right once, and your team can focus on features without worrying about secrets in the browser.