Your NEXT_PUBLIC_ environment variables are public (VITE_ too)
A .env file feels private, but the prefix decides who sees the value. NEXT_PUBLIC_ environment variables, and anything that starts with VITE_, REACT_APP_ or EXPO_PUBLIC_, get copied into the JavaScript bundle at build time, and every visitor downloads that bundle.
QUICK CHECKLIST
- No OpenAI, Anthropic or Stripe secret keys behind a public prefix
- No Supabase service_role key in client code
- No dangerouslyAllowBrowser: true
- Search the built bundle for sk- after every deploy
- Rotate any key that ever shipped
What the prefix does
The frameworks say so in their own docs. Vite: "VITE_* variables should not contain sensitive information such as API keys." Next.js: a NEXT_PUBLIC_ value "will be inlined into any JavaScript sent to the browser." Create React App: environment variables "are embedded into the build, meaning anyone can view them by inspecting your app's files." Expo: "Do not store sensitive info, such as private keys, in EXPO_PUBLIC_ variables." Apps from Lovable and Bolt are Vite apps, so the VITE_ rule applies to them (see Bolt app security).
Finding the key takes a minute. Open the site, open dev tools, search the loaded scripts for sk- or key.
How it happens in AI-built apps
Server-only variables read as undefined in the browser, so the fetch fails. The quickest fix an AI assistant finds is to add the public prefix. The error goes away and the key ships to the browser.
OpenAI's JavaScript SDK refuses to run in a browser until you set dangerouslyAllowBrowser: true. Its README says that option "exposes your secret API credentials in the client-side code." If you see that flag next to VITE_OPENAI_API_KEY, your key is public.
Which keys can live in NEXT_PUBLIC_ environment variables
| Fine in the browser | Never in the browser |
|---|---|
| Supabase anon or publishable key | Supabase service_role or secret key |
Stripe publishable key (pk_) | Stripe secret key (sk_live_) |
| Firebase web config | OpenAI and Anthropic API keys |
| Maps keys restricted to your domain | Database URLs with a password, email API keys |
Supabase's docs say anyone can read the anon key, so it only reaches what Row Level Security allows, while the secret key bypasses RLS and must never leave your control. Supabase is replacing the old anon and service_role keys with publishable and secret keys by the end of 2026. The rule stays the same.
How to fix a key in a public env variable
Rotate the key
Create a new key in the provider's dashboard and revoke the old one. Old builds and deploys still contain the old value, so handle it like any leaked API key.
Drop the public prefix
Rename the variable without
NEXT_PUBLIC_orVITE_, so only server code can read it.Move the call to the server
Use an API route, a server action or a Supabase Edge Function. The browser calls your route, and your route calls the provider. Give that route its own auth check.
Check the build output
Rebuild, redeploy, then search
dist/or.next/static/for the key prefix to confirm it's gone.
// app/chat.tsx
"use client"
const openai = new OpenAI({
apiKey: process.env.NEXT_PUBLIC_OPENAI_KEY,
dangerouslyAllowBrowser: true,
})// app/api/chat/route.ts
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
})
export async function POST(req: Request) {
// check the session, then call OpenAI
}Fix prompt you can paste
Find every environment variable with a VITE_, NEXT_PUBLIC_, REACT_APP_ or EXPO_PUBLIC_ prefix that holds a secret (API keys for OpenAI, Anthropic, Stripe secret keys, Supabase service_role/secret key, database URLs, email APIs). For each one: rename it without the public prefix, move the code that uses it into a server route or server action, and call that route from the client instead. Also check every "use client" file for server secrets. Remove dangerouslyAllowBrowser. List every key I must rotate.
How Leakcade checks this
Our client-env check reads your repo as text. It flags secret-looking variables with a public prefix, in code and in committed .env files, as critical. It also flags server secrets and Supabase service-role clients inside "use client" files. Anon and publishable keys, Maps keys and plain URLs are left alone.
In a small spot test on public GitHub files (not a rate), the check flagged all 9 files that sent VITE_OPENAI_API_KEY to the browser and stayed quiet on 3 files that used the service role key correctly on the server.
Further reading
- Vite docs: Env variables and modes
- Next.js docs: Environment variables
- Create React App: Adding custom environment variables
- Expo docs: Environment variables
- Supabase docs: Understanding API keys
- OpenAI Node SDK README
FAQ
Are NEXT_PUBLIC_ and VITE_ variables secure?
They're public by design. Next.js copies their values into the JavaScript it sends to the browser, so only put values there that you'd be fine printing on your homepage.
Can I hide an API key in a React or Vite frontend?
No. Anything the browser needs to make the call, the user can read. Move the call to a server route or edge function and keep the key there.
Is the Supabase anon key safe to expose?
Yes, as long as RLS is on and your policies are tight. The anon key can only do what your policies allow.
Do I need to rotate a key after removing the prefix?
Yes. Earlier builds and cached bundles still contain the old value, so treat it as leaked.
Want a second pair of eyes?
We review your whole repo and write a fix prompt for every finding. $19, report within an hour.
Check my repoLeakcade is an automated check. It can miss vulnerabilities and is not a substitute for a professional security audit or pentest. Provided as is. You stay responsible for your code. Where the law allows, our liability is limited to what you paid ($19). Terms