How to store API keys safely — and why a frontend .env isn't a secret

The most expensive beginner mistake in vibe coding isn't a logic bug. It's a leaked API key. Someone wires up a paid service (OpenAI, maps, SMS), drops the key "in .env, like everyone says," deploys the frontend — and a week later gets a bill for hundreds of dollars. The key was in plain sight the whole time.
Here's the trap: a frontend .env is not a secret. At build time its values get baked straight into the JavaScript every visitor downloads. Open DevTools → Sources, search for your key — and you'll find it there. Let's walk through, step by step, how to store keys so they can't be stolen.
Step 1. Understand where your code runs
This is the key step — everything else follows from it. Code lives in two places:
- Client (browser/app) — anything that ends up here is visible to the user. Any key here is public by definition.
- Server (backend, edge function) — runs on a machine the user can't reach. Secrets go only here.
The rule that decides everything: a secret key must live and be used only on the server. If an external API is called from the browser, the key will leak — no way around it.
Step 2. Split keys into public and secret
Not all keys are equal. Before hiding anything, sort them:
- Public keys are made to be in the browser: Supabase anon key, Stripe publishable key, a domain-restricted maps key. Their exposure is fine — they're protected another way (access rules, domain binding).
- Secret keys give full access: Stripe
sk_key, Supabase service-role, OpenAI key. These must never reach the client.
If unsure — check the service's docs: they state plainly which key is "secret" and which is "publishable/public."
Step 3. Move the secret into a server environment variable
Don't write the key as a string in code. Move it into an environment variable — and read it on the server:
// server code (edge function, backend) — NOT the browser
const key = process.env.OPENAI_API_KEY;
Frameworks have an important naming detail. Variables with a prefix like NEXT_PUBLIC_ or VITE_ are deliberately bundled into the browser. Secrets must not be named that way — or you'll ship them to the client yourself. A secret = a name without a public prefix.
Step 4. Keep .env out of git
The .env file must not enter your repository — otherwise the key stays in git history forever, even if you delete it later. Add to .gitignore:
.env
.env.local
And put a .env.example in the repo with empty values — a cheat sheet of which variables are needed. Empty, not real.
Step 5. Put secrets in your hosting panel
The local .env stays on your machine. In production, secrets are set through the hosting panel — there's a dedicated place for it:
- Vercel/Netlify → Settings → Environment Variables.
- Supabase → Edge Function secrets (
supabase secrets set).
The host injects them into the environment at runtime — they never enter your code.
Step 6. Proxy the external API through your server
So how do you call a paid API if the key can't go to the browser? Through a proxy: the browser calls your server, and the server — holding the key — calls the external API.
browser → your edge function (key here) → OpenAI API → response → browser
The key never leaves the server for a second. It's also a handy place to put limits so one user can't burn your whole budget. How such a call works technically is in the how to connect an API guide.
What you get
If you did all the steps: secret keys live only on the server, none are in git, on production they're in the hosting panel, and the browser reaches external services through your proxy. Opening DevTools, a user finds no secrets — only public keys, which is exactly what those are for.
And if a key does leak (you committed it, showed it on stream) — don't hide it, revoke and reissue it in the service panel immediately. Deleting it from code isn't enough: anything that's been in git or on air should be considered compromised. This check is part of the pre-launch security checklist.
FAQ
I added the key to .env — is that enough?
Depends on where that .env is. In the frontend (Vite, Next.js with NEXT_PUBLIC_) — no, the value ships in the bundle to the user. On the server (backend, edge function) — yes, as long as the file is in .gitignore and never entered git.
Can I just obfuscate the key in code so nobody finds it?
No. Obfuscation isn't protection: anything that runs in the browser can be unwound. Any dev tool will reveal the real value. The only real protection is not sending the secret to the client at all.
I accidentally committed a key — what now?
First reissue the key in the service panel — the old one becomes useless instantly. Only then clean git history. Order matters: while the old key is alive, it doesn't matter whether you removed it from code — it may already have been copied by bots that scan public repos.
Short story-lessons, an agent simulator and daily practice — in our mobile app. Free.





