Skip to content
← Back to the blog
securityapps

API key exposed in your frontend? How to tell and fix it

How to check whether a secret API key ships in your app's browser code, which keys are meant to be public, and how to move the secret ones to a server.

By Devonauts Team6 min read
Leer en español →
Dense lines of JavaScript code on a dark screen, seen at an angle

If a secret API key is in your frontend code, anyone who opens your site can read it, and hiding it in a file, an environment variable or minified JavaScript does not change that. The fix is to revoke the key, create a new one, and move every call that needs it to a server (an Edge Function, a serverless function or your backend) that the browser talks to instead. Public keys, such as a Stripe publishable key or a Supabase publishable key, are designed to be visible and are not the problem.

This guide shows how to check your own app from the outside, how to tell a public key from a secret one, what the fix looks like step by step, and how to stop it from happening again with the next AI prompt.

How do I check if my app exposes an API key?

You can do this in ten minutes with nothing but a browser. Do it on the live site, not the preview, because that is what visitors get.

  1. Open your app, right-click and choose Inspect, then open the Sources tab (in Firefox, Debugger).
  2. Press Ctrl + Shift + F (or Cmd + Option + F on a Mac) to search across every loaded file.
  3. Search for the prefixes that secret keys usually start with: sk_live, sk_test, sk-, sb_secret_, service_role, secret, Bearer.
  4. Switch to the Network tab, use the app normally (log in, run the main feature) and click on requests. Look at the request headers: if your browser sends an Authorization header straight to a third-party API such as OpenAI or SendGrid, the key is in the browser.
  5. Check the source code too. In a Vite project, every variable starting with VITE_ is copied into the browser bundle. In Next.js, the same goes for NEXT_PUBLIC_. If a secret lives in one of those, it is public.

If you find a match, assume it has already been copied. Automated scrapers look for exactly these strings.

Which keys are safe in the browser and which are not?

Not every key you find is a leak. Some services split their keys into a public half and a secret half on purpose.

KeySafe in browser?Why
Stripe publishable key (pk_live_…)YesIt can only start payments, not read or refund them
Stripe secret or restricted key (sk_…, rk_…)NoIt can read customers, issue refunds, move money
Supabase publishable or legacy anon keyYes, with Row Level Security onIt only gets what your policies allow
Supabase secret or service_role keyNoIt bypasses Row Level Security entirely
Firebase web configYes, with security rulesAccess is decided by your rules
Google Maps browser keyYes, if restrictedLock it to your domains and to the APIs you use
OpenAI, Anthropic, Gemini and other AI keysNoAnyone can spend your credit with them
Email, SMS and database passwordsNoFull access to send or read on your behalf

A "public" key is only safe if the thing behind it is locked. A Supabase publishable key with Row Level Security switched off is as dangerous as a secret key, because the database answers anyone. Our guide on whether the Supabase anon key is safe to expose goes into that case.

Why do AI builders put secret keys in the frontend?

Because it is the shortest path to a working preview. When you ask for "add AI summaries" or "send a welcome email", the quickest code calls the provider directly from the React component with the key pasted next to it. It works on the first try, so nothing tells you it is wrong.

Two other patterns cause the same result:

  • A secret was added as VITE_OPENAI_KEY or NEXT_PUBLIC_STRIPE_SECRET because the code could not read it without the prefix.
  • A key was pasted into a prompt or a config file and ended up committed to GitHub, where it stays in the history even after you delete the line.

How do I fix an exposed API key?

Order matters here. Moving the code first and revoking later leaves the old key working in the meantime.

1. Revoke and replace the key

Go to the provider's dashboard, create a new key, and revoke or delete the exposed one. Removing the key from your code does nothing on its own: the copy someone already took still works until the provider disables it. If the key is used by something you cannot update right away, create the new one first, switch over, then revoke the old one the same day.

2. Check what the key was used for

Look at the provider's usage or logs page for the days the key was public. Unusual spikes, requests from countries you don't serve, or charges you don't recognise mean it was used. For AI keys, our guide on what to do when an OpenAI key leaks covers the billing side.

3. Move the call to a server

Create a small server-side function that does the one thing the frontend needs: "summarise this text", "send this email", "create a checkout session". The browser calls your function; your function holds the key and calls the provider.

Browser  ->  /functions/summarise  (checks the user is signed in)
                  |
                  v
           AI provider  (key read from a server-side secret)

The key goes into the function's secret store (Supabase Edge Function secrets, Vercel or Netlify environment variables without the public prefix) and never into a file the browser loads.

4. Protect the new function

A server function that anyone can call is a free proxy to your paid account. Make it check that the request comes from a signed-in user, validate the input size, and limit how often each user can call it. Our guide on rate limiting AI features shows how.

5. Clean up the history

If the key was ever committed to Git, removing it from the latest version is not enough. Rotating it (step 1) is what actually protects you; rewriting history is optional tidy-up. GitHub's guide to remediating a leaked secret explains both.

How do I confirm the fix worked?

Repeat the check from the first section on the live site after redeploying. Search the Sources tab again for the old prefixes, watch the Network tab to confirm the browser now calls your own function rather than the provider, and test that the old key is rejected by the provider. Finally, call your function while signed out: it should refuse.

How do I stop it from happening again?

  • Keep a short list of which variables are public and which are secret, and never give a secret a VITE_ or NEXT_PUBLIC_ prefix.
  • Add a line to your AI tool's project rules: "Never call paid third-party APIs from the browser; create a server function."
  • Turn on secret scanning and push protection in GitHub so a commit containing a known key format is flagged.
  • Set spending limits and alerts with each provider, so a leak shows up as an alert rather than a bill.

Our security checklist for AI-built apps puts this check alongside the others worth doing before launch.

Quick answers

Is a key in an environment variable automatically safe? No. It depends on where the variable is used. If the frontend build reads it, it ends up in the browser bundle.

Can I hide the key by obfuscating or encrypting it in JavaScript? No. The browser has to decrypt it to use it, so anyone can do the same.

Is the Stripe publishable key a leak? No. It is meant to be public. Only secret and restricted keys must stay on the server.

Do I need to delete my GitHub repository? No. Revoke the key with the provider. Deleting or rewriting the repository does not disable a copy someone already made.

Want someone to check it for you?

The free health check looks at what your live app exposes from the outside, including keys in the browser bundle. If you want a person to go through the code, move the calls to a server and review your database rules, our Supabase security review and maintenance plans cover that work, or you can tell us what you found.

Still stuck with your app?

Run the free health check on your app's URL, or get the $299 App Rescue Audit: a senior engineer reviews the code and sends you a written report in 2 business days.