
How to Hide API Keys in Frontend JavaScript (Without the Dangerous Myths)
Mahmud Hasan
October 14, 2026
Your .env file doesn't do what you think it does
Somewhere in the last few years, frontend developers converged on a ritual: put the API key in .env, add .env to .gitignore, and announce the secret "hidden." One popular tutorial — still ranking on page one of Google for this exact question — walks through the whole ritual and then declares, in so many words, that's it, your keys are hidden and safe.
It is not. If your key sits behind VITE_ in a Vite project, or NEXT_PUBLIC_ in Next.js, or any of the other "expose to client" prefixes, the build inlines the literal value into the JavaScript bundle. Your deploy serves it to every visitor inside dist/assets/index-abc123.js. The .env file never touches git, and the secret still ends up on the public internet — one DevTools Source tab away from anyone curious.
The confusion is understandable, because the tooling almost encourages it. Vite reads .env at startup and quietly inlines anything with the public prefix. Next.js does the same during next build. Neither minification, base64 encoding, nor turning off source maps changes this; the string is sitting there in the shipped JS in plaintext. A frontend .env file is configuration management, not secret management. If the browser needs the value, assume the user can see it — because they can.
The damage is real, and embarrassingly old
This isn't a theoretical footnote. Truffle Security scanned 224 million public GitHub repositories this year and tested what they found against the issuing services. 543,699 credentials still worked. The median one had been sitting in public code for 784 days. The oldest still-valid credential they found dated back to June 2009 — sixteen years of silent exposure.
Among the haul: 31,374 live Google Gemini API keys (median leak date: February 2025), 51,067 MongoDB connection strings, 9,189 SendGrid keys, 6,819 AWS access keys. GitGuardian's 2026 report found that 28.6 million new secrets were leaked into public GitHub commits in 2025 alone, and leaked AI-service keys grew 81% in a single year — bots find a leaked key within minutes.
And the platform won't save you either. GitHub's push protection — on by default since early 2024 — cut one class of leaks by about half, but nearly 200,000 of the still-active credentials were committed after it became the default, because over half of the surviving credentials are types it doesn't block: database connection strings, private keys, Google API keys. One leaked CDC private key sat valid for seventeen months. The key worked, so nothing ever complained.
The only fix that actually works
Here is the sentence you came for: you cannot hide an API key in the frontend. Anything the browser downloads is the user's to read. The only real answer is architectural — the key never leaves the server.
The standard shape of the fix is a proxy: your frontend calls your own endpoint, and your endpoint — running server-side, where process.env is actually secret — calls the third party with the key. A Next.js route handler takes about fifteen lines:
// app/api/weather/route.ts
// Runs on the server. WEATHER_API_KEY never reaches the browser.
export async function GET(request: Request) {
const city = new URL(request.url).searchParams.get("city") ?? "london";
const res = await fetch(
`https://api.weatherapi.com/v1/current.json?key=${process.env.WEATHER_API_KEY}&q=${encodeURIComponent(city)}`
);
if (!res.ok) return Response.json({ error: "upstream failed" }, { status: 502 });
const data = await res.json();
// Strip anything your UI doesn't need before returning it.
return Response.json({
city: data.location.name,
tempC: data.current.temp_c,
condition: data.current.condition.text,
});
}
The browser side becomes gloriously boring:
// No key anywhere in this file.
const res = await fetch(`/api/weather?city=${encodeURIComponent(city)}`);
const { tempC, condition } = await res.json();
No backend yet? A serverless function is the same idea — Netlify, Vercel, Cloudflare Workers all give you server-side environment variables for free, and the free tier is more than enough for a personal project. Two refinements make the proxy production-worthy: rate-limit the endpoint (it's now the choke point — treat it like one), and require your users to be logged in before it proxies to a billable API, so one anonymous caller can't spend your quota.
The middle ground: keys that are meant to be seen
Not every key is a secret. Stripe's publishable keys (pk_...), Google Maps keys, Firebase client keys — these are designed to ship in client code. The protection for them isn't secrecy; it's restrictions. Stripe's secret key can charge money; the publishable key can't. A Maps key with HTTP-referrer restrictions and a quota cap will happily sit in a bundle all day.
That's the mental model that actually works: don't ask "is this key hidden?" Ask "what is the worst thing this key can do, and have I limited that?" A scoped, referrer-restricted, quota-capped key in client code is fine. A full-access billable key in client code is a leak with extra steps. And if a key has already shipped — deleting it from the next build doesn't revoke it. Only rotation does. Check your issuer's dashboard, revoke, reissue, and treat the old one as public forever.
What to do this week
If you suspect your project is one of the ones doing this wrong, the audit takes twenty minutes:
- Search your built bundle, not your source. Open your site's JS file and Ctrl+F for a fragment of your key. If it's there, it's shipped. (Never search your source for it and stop — the build is what the world sees.)
- Rotate anything exposed. Revoke in the provider dashboard and reissue. There is no "probably nobody looked" — assume bots did.
- Move the call behind one server-side endpoint. Start with a single route handler like the one above; you don't need to redesign your API.
- Scan your git history, not just the current tree. Secrets committed two years ago are still valid until revoked — the Truffle study proved the median one was 784 days old.
- Add the right check going forward. GitHub's push protection catches common patterns, but not database URIs or Google keys — run a local scanner or pre-commit hook for the ones it misses.
The ritual of .env plus .gitignore feels like security, which is exactly why it's dangerous. Keys don't leak where you put them — they leak where your build sends them. Once you see that, the fix is obvious, and it's only about fifteen lines long.
References
- Truffle Security credential-leak study — via Cybernews: Exposed GitHub Credentials: 543,699 Live Secrets Found
- GBHackers writeup: Researchers Find 543,699 Active Credentials Leaked in Public GitHub Repos
- Linuxiac coverage: Over Half a Million Credentials Leaked on GitHub Remain Active
- GitGuardian "State of Secrets Sprawl 2026" — via RepoDx: repodx README
- GitGuardian GitHub App key study — via SecureInSeconds: 474 GitHub App Keys Still Work
- Vite env-variable documentation — community reference: vite/SKILL.md — Security
- freeCodeCamp: How to Hide API Keys in Frontend Apps using Netlify Functions
- Corsfix: How to Hide API Keys in Frontend Code
- dev.to: Your .env Is Not Where Your Secrets End Up
Comments
More in Web Development

Workday Bought C1.ai to Give AI Agents a Login. Here's What Agentic Identity Management Actually Is
Workday just agreed to buy C1.ai, an identity platform for AI agents. Here is what agentic identity management actually does — and why your agents logging in as you is the part that should worry you.
Read more