Is my API key exposed in my JavaScript? VITE_ and NEXT_PUBLIC_ explained
If your code reads an API key through a variable starting with VITE_, NEXT_PUBLIC_ or REACT_APP_, that key is in your public JavaScript and anyone can copy it. Putting it in an environment variable keeps it out of your Git repo, but it doesn't keep it out of the browser: the build tool pastes the value into the bundle. Public-by-design keys (Supabase anon/publishable, Stripe pk_, Firebase web config) are fine there. Secret keys (OpenAI, Stripe sk_, Supabase service role, Resend, AWS and so on) must only be used on a server or in a serverless/edge function.
Why the prefix matters
Build tools deliberately expose prefixed variables to the browser:
- Vite (used by Lovable and Bolt): "Variables prefixed with
VITE_will be exposed in client-side source code after Vite bundling", and the docs warn they "should not contain sensitive information such as API keys" (Vite docs). - Next.js (v0, many Cursor projects):
NEXT_PUBLIC_variables are inlined into the JavaScript sent to the browser at build time (Next.js docs). - Create React App: the same applies to
REACT_APP_.
Hardcoding a key in a component is the same problem with extra steps. If the browser runs the code, the browser has the key.
Safe in the browser vs secret
| Usually fine in frontend code | Never in frontend code |
|---|---|
Supabase anon / sb_publishable_ key (with RLS) | Supabase service_role / sb_secret_ |
Stripe publishable key pk_live_… | Stripe secret key sk_live_… or restricted key rk_… |
Firebase web config (apiKey, projectId) | Firebase Admin SDK service account JSON |
| Public analytics IDs, map keys restricted by domain | OpenAI, Anthropic, Gemini, Resend, SendGrid, Twilio, AWS keys, database URLs |
Sources: Supabase API keys, Stripe API keys, Firebase API keys. Firebase notes its web API key isn't used for authorization, but it also recommends restricting keys and keeping other, billable Google APIs off it.
How to check your live site in two minutes
- Open your live site, then DevTools (F12) → Sources. Press Ctrl/Cmd+Shift+F to search all files.
- Search for:
sk-,sk_live,rk_live,service_role,sb_secret_,AKIA,re_(Resend),SG.(SendGrid),-----BEGIN. - Also check the Network tab: if the browser calls
api.openai.comdirectly, the key is in the browser. - Locally, after
npm run build, search the output folder (dist/or.next/) for the same strings.
The fix: call the API from the server
The browser calls your endpoint (an API route, serverless function or Supabase Edge Function), and that endpoint calls the third-party API with a key read from a server-only environment variable (no public prefix). Add a login check and a rate limit so your endpoint doesn't become a free proxy for strangers.
"Public" keys still need limits
A key being fine in the browser doesn't mean it should be unlimited. A few cheap settings make a leaked or abused key much less painful:
- Map and other Google API keys: restrict each key to your own domains (HTTP referrer restrictions) and to only the APIs it needs. Google recommends both in its API security best practices.
- Stripe on the server: for scripts, automations and AI tools that only need part of your account, use a restricted API key with just the permissions required, instead of your full secret key.
- AI APIs: keep keys on the server, use separate keys per project or environment so you can rotate one without breaking everything, and set usage limits and alerts in your provider's dashboard. OpenAI covers this in its production best practices.
- Your own endpoint: once the secret lives behind
/api/…, that endpoint is the new thing to protect. Require login, rate-limit per user, and validate input so it can't be used as a free proxy.
Already leaked? Do this in order
- Rotate the key in the provider's dashboard (create a new one, delete the old one).
- Set a spending limit or usage alert if the provider supports it.
- Check the provider's usage logs for activity you don't recognise.
- Deploy the server-side fix so the new key never reaches the browser.
- If the key was ever committed to Git, assume it's public even after you delete the file, because it stays in the history. GitHub's secret scanning can warn you about this.
FAQ
Are environment variables secure in Vite or Next.js?
Only the ones without a public prefix, used in server-side code. Variables starting with VITE_ or NEXT_PUBLIC_ are deliberately copied into the JavaScript sent to every visitor, so they must only hold values that are safe to be public.
Is it OK to put my OpenAI API key in VITE_OPENAI_API_KEY?
No. Anything in a VITE_ variable ends up in your public bundle, and anyone can copy the key and use it on your bill. Call OpenAI from a server-side route or edge function that reads the key from a server-only variable.
How do I know if my API key is exposed?
Open your live site, then DevTools Sources, and search all files for key prefixes such as sk-, sk_live, service_role or sb_secret_. Also check the Network tab for direct calls from the browser to providers like api.openai.com.
Is my Stripe publishable key safe to expose?
Yes. Stripe publishable keys (pk_) are meant for the browser. The secret key (sk_) and restricted keys (rk_) must stay on the server.
I removed the key from my code. Is that enough?
No. Once a key has been public, assume someone has it. Rotate it in the provider's dashboard and check usage for anything unexpected.
Sources
- Vite: Env variables and modes
- Google Maps Platform: API security best practices
- Stripe: Restricted API keys
- OpenAI: Production best practices
- Next.js: Environment variables
- Stripe: API keys
- Supabase: API keys
- Firebase: API keys
- GitHub: About secret scanning
This guide is general information, not a security audit. Platforms change, so check the linked docs for current details. Spotted something out of date? Email hello@milokits.com.