Are your API keys exposed? VITE_, NEXT_PUBLIC_ and the front end

Updated

Are my API keys exposed if I put them in a VITE_ or NEXT_PUBLIC_ variable?

Yes. Vite copies every VITE_ variable and Next.js copies every NEXT_PUBLIC_ variable into the JavaScript your visitors download, so anyone can read them. Public keys (Supabase anon, Stripe publishable, Firebase config) are fine there. Secret keys (Stripe secret, OpenAI, Anthropic, Supabase service role) must only live on a server. If one was exposed, rotate it now: deleting it from the code is not enough.

Why the prefix makes a key public

Your app has two halves: code that runs on your server and code that runs in the visitor's browser. The browser half is downloaded by everyone. Vite and Next.js only put environment variables into the browser half when they carry the public prefix, so the prefix is literally the instruction "ship this to everyone".

AI tools often add the prefix because the code fails without it. The error goes away, the key is now public, and nothing warns you.

Safe in the front end vs never in the front end

Safe because they are designed to be public (your data is protected by other rules):

  • Supabase URL and anon / publishable key (protected by row level security)
  • Stripe publishable key, pk_live_ or pk_test_
  • Firebase web config, including its apiKey (protected by security rules)
  • Analytics and error-monitoring public keys

Never in the front end

Each of these gives a stranger your account's full power or your bill:

  • Supabase service_role / secret key: reads and changes every row, ignoring row level security
  • Stripe secret key sk_live_: refunds, payouts and customer data
  • OpenAI (sk-proj-), Anthropic (sk-ant-), Gemini or other AI keys: charged to you per request
  • Email or SMS provider keys (Resend, SendGrid, Twilio): spam sent in your name
  • Database connection strings and JWT signing secrets

Move the call to the server

The fix is to call the paid service from a server function and let the browser call your function instead, after checking the user is logged in and within their limit.

// Before (browser): anyone can copy the key
const openai = new OpenAI({ apiKey: import.meta.env.VITE_OPENAI_KEY, dangerouslyAllowBrowser: true });

// After (browser): call your own server function
const { data } = await supabase.functions.invoke('chat', { body: { prompt } });

// Server function: the key comes from a secret, never from the browser
const openai = new OpenAI({ apiKey: Deno.env.get('OPENAI_API_KEY') });

If a secret key was exposed

  • Rotate it in the provider's dashboard first. The old key stays valid until you do, and it is already in your git history and in old deployed builds.
  • Check the provider's usage page for requests you did not make.
  • Put the new key in a server-side secret (Supabase secrets, Vercel or Netlify environment variables without the public prefix).
  • If the key was ever pushed to a public GitHub repository, assume it was copied: bots scan new commits within minutes.

Questions

Is it safe if my repository is private?

The repository being private does not help: the key is in the JavaScript your live site sends to every visitor.

Can I hide a key by encoding or splitting it?

No. The browser has to reassemble it to use it, so anyone can too. The only safe place is a server.

What does OpenAI's dangerouslyAllowBrowser option do?

It turns off the SDK's warning about using a secret key in the browser. If your code needs it, the key is public.