How to Check for Exposed API Keys: NEXT_PUBLIC_ and VITE_ Variables Ship to the Browser
By the IDEAVALU team · Updated
Any JavaScript that reaches the browser can be read with developer tools. If it contains a secret API key, a stranger can call the API as you, run up your bill or delete your data. This is especially common in AI-generated code, which often puts keys straight into the frontend.
How secret keys end up public
The most common cause is the environment variable name. Next.js inlines variables starting with NEXT_PUBLIC_, Vite inlines VITE_, and Create React App inlines REACT_APP_, writing their values into the JS so the browser can use them. Give a secret one of these prefixes and it becomes public the moment you deploy.
The other cause is calling the OpenAI or Stripe API directly from the browser. The browser needs the key to make the call, so the key ships with your code. Keep any code that uses a secret key on the server (API Route, Server Action, Edge Function) and have the browser call your server instead.
Keys that are safe to publish, and keys that are not
Some services issue keys designed to live in the browser. Here is how to tell them apart.
- Safe in the browser: the Supabase anon or publishable key (sb_publishable_), the Firebase web apiKey and the Stripe publishable key (pk_live_). This assumes you protect your data with Supabase RLS or Firebase security rules.
- Never in the browser: the Supabase service_role or secret key (sb_secret_), Stripe secret keys (sk_live_) and restricted keys (rk_live_), OpenAI and Anthropic API keys, AWS access keys, GitHub tokens, Slack and Discord webhook URLs, and email provider API keys.
- Keys such as the Google Maps API key are meant to be public, but restrict them to your domains (HTTP referrers) in the provider console.
Check it yourself
Searching the build output catches leaks before you deploy. You can also search all files in the Sources panel of your browser developer tools (Ctrl+Shift+F on Windows, Cmd+Option+F on Mac).
Search a Next.js build
npm run build
grep -rEo "sk_live_[0-9A-Za-z]{8}|sk-(proj-)?[A-Za-z0-9_-]{8}|sb_secret_|service_role" .next/static | sort -uSearch a Vite build
npm run build
grep -rEo "sk_live_[0-9A-Za-z]{8}|sk-(proj-)?[A-Za-z0-9_-]{8}|sb_secret_|service_role" dist | sort -uPrevent it from happening again
In Next.js, put import 'server-only' at the top of any file that reads a secret. If that file is ever imported from client code, the build fails. Turning on GitHub secret scanning with push protection stops commits that contain keys before they are pushed.
A file that uses a secret key (Next.js)
import 'server-only'
export async function createCompletion(prompt: string) {
// No NEXT_PUBLIC_ prefix on process.env.OPENAI_API_KEY
const apiKey = process.env.OPENAI_API_KEY
// ...
}How to fix
Secrets in public JavaScript
A key that reached the browser is already readable by anyone. Do not just delete it: revoke it and create a new one.
1. Revoke (or roll) the key in each service's dashboard right away.
2. Use the new key only on the server (API Route, Server Action, Edge Function) and have the browser call that server.
3. Check your environment variable names. Variables starting with NEXT_PUBLIC_ (Next.js), VITE_ (Vite) or REACT_APP_ (Create React App) are embedded in the JS at build time. Never use these prefixes for secrets.
4. A leaked Supabase service_role key bypasses all RLS. Reissue it and check whether data was changed.
5. If the key was committed to Git, it is in the history too. Revoke it first, and for public repositories consider rewriting history.
FAQ
- Is it a problem that my Supabase anon key is in the JS?
- No. The anon and publishable keys are designed for the browser. What matters is that RLS stops anyone from reading or writing other users' rows. If a service_role or secret key is in there, revoke it immediately.
- Is it enough to remove the key and redeploy?
- No. Someone may have copied it while it was public, so always revoke the key in the service dashboard and create a new one. It also remains in your Git history and in old JS cached by CDNs.