Where to put your API keys when you code with AI

Dropping an API key straight into your code is something everyone does at first — me included. It works on the first try, so you move on. The catch is that those keys love to travel: all the way into your visitors' browsers, or into the history of your public Git repo.

"In the code" is not a hiding place

When you ask an AI assistant to wire up an API, it usually answers with the shortest path: const apiKey = "sk-..." at the top of the file. That's not a mistake on its part — it's optimizing for "make it work now." The problem is what happens to that line next.

Two things we forget easily:

  • Code isn't secret. If it runs in the browser, anyone can read it (right click, "Inspect," Sources tab). A key written there is as public as the text on your page.
  • Git never forgets. You commit the key, you delete it in the next commit… it stays in history, reachable by anyone who clones the repo. Removing it "visually" isn't enough.

The real question: client or server?

This is the distinction that changes everything, and once it clicks, the rest follows.

  • Client-side code is downloaded and run by every visitor's browser. Everything in it is visible.
  • Server-side code runs on a machine you control. What lives there is never shipped to the visitor.

The classic front-end trap: the REACT_APP_, VITE_, and NEXT_PUBLIC_ prefixes. People assume an environment variable is "hidden" by nature. Those prefixes mean the exact opposite: "copy me into the bundle sent to the browser." The key ends up in plain text in the shipped JavaScript.

// ❌ In a React component: the key ships in the browser bundle fetch("https://api.openai.com/v1/chat/completions", { headers: { Authorization: Bearer ${import.meta.env.VITE_OPENAI_KEY} }, });

The fix isn't a better variable name — it's moving the call. You create a small server-side route that holds the key and forwards the request.

// ✅ Server-side: the key stays on your machine, never exposed app.post("/api/chat", async (req, res) => { const r = await fetch("https://api.openai.com/v1/chat/completions", { method: "POST", headers: { Authorization: Bearer ${process.env.OPENAI_KEY} }, body: JSON.stringify(req.body), }); res.json(await r.json()); });

On the front end, you stop calling the API directly: you call your own /api/chat. Bonus: you can add rate limiting or a check there — something you could never do with a key loose in the browser.

The .env + .gitignore duo

For local development, secrets go in a .env file at the project root:

OPENAI_KEY=sk-proj-abc123... DATABASE_URL=postgres://...

And crucially, you tell Git to ignore it with a line in .gitignore:

.env .env.local

One last detail that makes the difference: a .env.example file — this one committed — with the variable names but no values.

OPENAI_KEY= DATABASE_URL=

That way the next person — or your AI assistant — knows which keys are needed without ever seeing the real ones. It's the kind of detail that separates a hacked-together project from a clean one.

Where the real keys live in production

Your .env stays on your machine. In production, the real values go into your host's environment variables. Vercel, Netlify, Railway, Render, Fly — they all have an "Environment Variables" screen in their dashboard. You paste in OPENAI_KEY and its value, and your code reads it with process.env.OPENAI_KEY, exactly like locally.

The thing to remember: the key is never in a file that travels. It's injected by the environment, right where the code runs.

The before-push reflex

Before every git push, two commands are worth ten minutes of stress:

git status # what's actually going out? git diff --staged # reread, line by line, what you're about to commit

If you see your .env in the list, stop. And if a key already got pushed once? The right move isn't to hide it — it's to rotate it (regenerate it) with the provider. A key that has touched a public repo should be treated as burned; revoking one and creating another takes thirty seconds.

GitHub also gives you a safety net: it scans for known secrets and can block a push or notify the provider, which sometimes disables the key on its own. Reassuring — but it's a net, not a plan.

Wrapping up

None of this slows you down. It's three habits: ask "where does this code run?", keep secrets in .env + .gitignore, and take a look before you push. The AI writes the code in ten seconds; you decide where the keys live. That's exactly what vibecoding like a pro looks like.