Secret Management for Vibe Coders: The System I Wish I Had a Year Ago
Yesterday Vercel announced a security incident. This morning I sat down with my project and realized I didn't have a clear system for what to do next. I knew I was supposed to "rotate my keys." I'd done it before — because an AI told me to. But I'd never really understood why, in what order, or whether I'd been storing secrets properly this whole time. I've been building with AI tools for just over a year. No CS degree, no coding books, no deep knowledge of any single language. Everything I know came from prompting Claude, Gemini, Cursor, and building through practice. I've shipped real things. But secret management was always something I followed by instinct rather than actual understanding. Today I fixed that. Here's the full picture — plain English, no fluff — so you can skip straight to the good system. Worth getting clear on this because most tutorials treat the whole .env file as one scary blob. There are actually three distinct types of values: Real secrets — if someone gets these, they can impersonate your app, rack up your API bills, or read your users' data: Database passwords API keys (your AI provider, payment processor, etc.) The key your app uses to sign user sessions Anything called a "signing key" or "encryption key" Config values — totally safe to share, they just control how your app behaves: A model name like GEMINI_MODEL=gemini-3.0-flash Feature flags, version numbers, non-sensitive settings Public values — stuff you intentionally expose to the browser. In Next.js these start with NEXT_PUBLIC_. Don't put anything sensitive here — it ends up in your frontend JavaScript where anyone can read it. Only the first category needs serious handling. The rest is just configuration. Here's the mental model that makes everything else make sense: One source of truth at the top — everything else is just a copy of it. Layer 1 — Your password manager is the boss. Every secret lives here with notes on what it is and when you last rotated it. I use Bitwarden (free, open source), but 1Password and Dashlane are equally solid. One folder per project. Layer 2 — Your hosting platform's env vars (Vercel, Railway, Render, Fly.io — they all have this). This is what your live app actually reads. Your code never touches these directly — it just calls process.env.YOUR_VARIABLE_NAME and the platform fills it in automatically at runtime. Layer 3 — .env.local on your laptop. A local copy for development only. It's gitignored — git completely ignores it, it never gets committed. You sync it from your hosting platform using their CLI. The rule: update the password manager first, then update the copies. That way there's always one place to go if something goes wrong. .env.example This is the habit that pays off on every project. .env.example is your .env.local with all real values removed — just the variable names and a comment about what each one is. You commit this to git. You share it with collaborators. You paste it into AI sessions. # .env.example # Copy this to .env.local and fill in real values from your password manager. # NEVER commit .env.local. NEVER paste real values into AI sessions. DATABASE_URL= # Your database connection string AUTH_SECRET= # Run: openssl rand -base64 32 GEMINI_API_KEY= # From Google AI Studio STRIPE_SECRET_KEY= # From your Stripe dashboard Variable names, no values, helpful comments. Safe to share anywhere. Start every new project by creating this file before you write a single line of code. Here's something I didn't think about carefully enough until today — and I suspect most AI builders haven't either. AI coding tools like Cursor and Claude Code index your codebase as context. If .env.local is open in a tab, those real values can end up in the AI's context window. That's not how you want your database password or Stripe key traveling around. Here's the before/after on what I changed: The AI only needs variable names to write correct code — it never needs the actual values. Three rules that make this a non-issue: Never open .env.local during an AI session. You don't need to. Manage values in your hosting dashboard, sync locally via CLI. Add .env.local to your AI's ignore file. Create a .cursorignore (and/or .claudeignore for Claude Code) in your project root: .env.local The AI indexer will never touch that file even if you accidentally open it. Start sessions by pasting .env.example. Just drop it in with a note: "here are my variable names, use process.env.VARIABLE_NAME in any code you write." The AI produces perfectly correct code and never needs to know a single real value. What "Rotating Keys" Actually Means Rotating means: generate a new secret at the provider, update it everywhere, revoke the old one. That's it. You do it when: A provider announces a security incident (hi, Vercel) Someone who had access leaves your team You want a regular security sweep (once a year is fine for small projects) The same five steps work for every secret: Generate a new value at the provider's dashboard Update your password manager first Update your hosting platform's env vars Redeploy Run vercel env pull .env.local (or your platform's equivalent) to sync locally Rotate in this order — highest damage potential first: Database password (full data access if leaked) Session/auth secrets (user account hijacking) Encryption keys ← read the warning below before touching these OAuth app secrets (account impersonation) Everything else (API keys, billing abuse) Encryption key warning If you're encrypting data in your database, you can't just swap the key — existing encrypted records become unreadable. The right move: version your keys (ENCRYPTION_KEY_V1, ENCRYPTION_KEY_V2), keep the old one available for decryption, and write a migration script to re-encrypt all records with the new key before retiring the old one. If you haven't encrypted any production data yet, rotate freely — no migration needed. This is the part I'd been fuzzy on: "Google handles security for me, right?" Not quite. Here's the actual picture: Your app has its own permanent identity with Google — a Client ID and Client Secret that live in your env vars just like any other API key. They prove to Google that requests are coming from your app. Then, when a user connects their account ("Sign in with Google"), Google issues two tokens that you store in your database: A short-lived access token (expires in hours) A long-lived refresh token (valid until the user revokes it) Google stores nothing on your behalf. You're responsible for those tokens in your database, which means your database security matters a lot for OAuth too. Four steps, five minutes, done: And every time you add a new secret going forward: Create the credential at the provider Add it to your password manager first Add it to your hosting platform's env vars Sync locally with the CLI Add the variable name (no value) to .env.example Two minutes per secret. Scales cleanly as your project grows. If you've accidentally committed an .env file, you'd probably remember — it tends to be a memorable moment. But if you want to be certain, run these in your project root: # Was any .env file ever committed? git log --all --full-history -- ".env*" # Search history for anything that looks like a real secret value git log --all -p | grep -E "(API_KEY|SECRET|PASSWORD)\s*=\s*['\"]?[a-zA-Z0-9]" Nothing returned — you're clean. If something does come back: rotate the exposed secret first (that's what actually protects you), then clean up the history with git filter-repo and force push. Rotating is the urgent action. The cleanup is secondary hygiene. Every secret your app uses is a credential that proves your app's identity to another service. Treat it like a password: store it in a password manager, keep it out of your codebase, rotate it after incidents, and don't hand it to an AI assistant. The system is four things: password manager (source of truth) → hosting platform (production copy) → .env.local (local copy) → .env.example (the safe version you share everywhere). Get these four right and you're doing secret management properly — the same way any experienced developer does it. No degree required. Just a bit of intentional setup at the start of each project. Now go create that .env.example. Building something with AI tools? I'd love to hear what you're working on — drop it in the comments. I write about vibe coding, building in public, and the AI-first dev workflow.
