Skip to content

How to Stop Exposing API Keys in Frontend JavaScript Code

I was testing an AI-generated login page when I opened DevTools and saw this in the Network tab:

DevTools Network Response
"openaiApiKey": "sk-proj-xxxxxxxx..."

I didn’t write that code — an AI coding tool did. And it happily embedded my OpenAI key directly into the frontend JavaScript bundle.

Why This Happens

AI coding tools generate what you ask for. If you prompt “build me a chat feature”, it generates a complete solution — often with the API key hardcoded in a frontend .js file or a .env that gets bundled into the browser. The tool doesn’t know the difference between frontend and backend unless you tell it.

The problem: browser DevTools shows every byte of your JavaScript. There’s no “obfuscation” that hides a string from a determined user. Tools that claim to “minify and protect” your keys are lying — any key in the browser is extractable.

❌ Bad: API key in frontend
// I found this in a generated file
const openai = new OpenAI({
apiKey: 'sk-proj-xxxxxxxxxxxxxxxxxxxxxxxxxxxx',
dangerouslyAllowBrowser: true,
});

The dangerouslyAllowBrowser: true flag isn’t there by accident. OpenAI’s SDK explicitly warns you not to use it in production. It exists only for prototyping.

The BFF Proxy Pattern

The fix is a Backend-For-Frontend (BFF) — a thin backend that holds the API key and proxies requests. The frontend talks to your backend, your backend talks to OpenAI.

Frontend: fetch from your API
async function generateContent(prompt) {
const response = await fetch('/api/generate', {
method: 'POST',
body: JSON.stringify({ prompt }),
});
return response.json();
}
Backend: proxy to OpenAI
// API key lives here, never reaches the browser
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
app.post('/api/generate', async (req, res) => {
const completion = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [{ role: 'user', content: req.body.prompt }],
});
res.json({ content: completion.choices[0].message.content });
});

The .env file on your server stays on your server. No .env gets bundled into the frontend build.

Firebase and Supabase Are Different

I see this confusion often: people panic when they see apiKey: 'AIzaSy...' in their Firebase config. That’s fine. Firebase and Supabase “API keys” are public client identifiers. Security for these services comes from Security Rules (Firebase) or Row Level Security (Supabase), not from hiding the key.

✅ Safe: Firebase client config
const firebaseConfig = {
apiKey: 'AIzaSy...', // public client identifier
authDomain: 'your-app.firebaseapp.com',
projectId: 'your-app',
};

This is the equivalent of a Stripe publishable key. Anyone can see it, but it can’t do anything dangerous on its own.

But never confuse this with admin/service account keys. A Firebase Admin SDK private key or a Supabase service_role key in your frontend is a disaster — it bypasses all security rules.

The Financial Risk

An exposed OpenAI key costs real money. A scraper finds your key in a bundle, calls GPT-4o thousands of times, and you get a bill for $2,000 before you notice.

  • Frontend keys: never for AI APIs, never for database admin access
  • Firebase/Supabase config keys: safe in frontend, enforce security via rules
  • Service account keys: never in frontend, ever

How AI Coding Tools Fit In

Most AI coding tools don’t understand deployment boundaries. They see “JavaScript project” and emit a single codebase. When you ask for a feature that needs an API, you must explicitly say “create a backend endpoint for this, the frontend calls the backend”. Or scaffold your project with a BFF structure upfront.

If you’re using an AI tool, add this to your prompt:

“Generate a backend API route for OpenAI calls. The frontend must never contain an API key.”

In this post, I covered why AI-generated frontend code often leaks API keys, how to use the BFF proxy pattern to keep keys on the server, and which “API keys” (Firebase, Supabase) are actually safe to expose in the browser.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments