Secrets and API keys: the exposed-key trap
A is anything that grants access: (Application Programming Interface) keys, passwords, payment provider tokens, signing keys. The single most common — and most expensive — vibe-coding mistake is leaking one.
Two traps catch people constantly:
- Secrets in client code. Anything in your (the JavaScript that runs in the browser) is public. Users can open dev tools and read it. The AI, asked to call an API from a React component, will happily paste your secret key right there — and now anyone who visits your site can copy it and run up your bill. Secret keys belong on the server, never in code that ships to the browser.
- Secrets in the repo. A key hardcoded in a file gets committed to . Even if you delete it later, it lives forever in the git history, and bots scan public repos for exactly this within minutes of a push.
The correct home for a secret is an — a value supplied to the app at runtime, kept out of the code entirely:
// VULNERABLE: key is in the source, will be committed to git
const stripe = new Stripe("sk_live_51H8xQ2eZvK...");
// SAFE: key is read from the environment, never written in code
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
A secret has exactly one safe home — server-side, read from the environment. The browser is a public place: anything shipped there can be read by anyone who opens dev tools.
┌──────────────────────────────────────────┐
│ .env (gitignored, never committed) │
│ STRIPE_SECRET_KEY=sk_live_... │
└───────────────────┬──────────────────────┘
│ injected at runtime
▼
┌──────────────────────┐ ┌──────────────────────┐
│ SERVER │ │ BROWSER (client) │
│ process.env.KEY ✓ │ ✗──────│ anyone can read │
│ calls Stripe here │ NEVER │ dev tools · "view │
│ │ send │ source" · network │
└──────────────────────┘ here └──────────────────────┘
safe: stays private public: = leaked
Add .env (and .env.local, etc.) to your .gitignore before you write a single secret. If a key ever does land in your code or history, treat it as burned: rotate it (generate a new one and revoke the old) — deleting the line is not enough, because the old key is still valid and still out there.