Draft, not yet published
Secrets management for vibe-coded apps: beyond .env files
7 July 2026· 6 min read · by Stackbastion
You’ve got a .env file with your database password, an API key or two, and a Stripe secret. It works. But now it’s on your laptop, in a note, maybe in a Slack message to yourself, and possibly in your git history. You know that’s not how real apps do it, but “use a secrets manager” sounds like a week of setup you don’t have. It isn’t. Here’s the middle path between a plain .env and an enterprise vault.
Why .env files stop being enough
A .env file is a great local convenience and a poor production system. The problems show up the moment more than one person or machine is involved.
It gets copied everywhere. To deploy, you paste it into a server. To onboard a teammate, you send it to them. Every copy is a place a secret can leak, and you have no idea how many copies exist or who has them.
It has no history and no rotation. When you change a key, nothing records that it changed, and every stale copy still holds the old value. When someone leaves the team, you can’t easily rotate everything they saw.
It ends up in git. This is the classic one. A missing .gitignore line and your secrets are in the repo history forever. We wrote about that failure in detail in the exposed keys 5-minute check, and it’s the single most common way vibe-coded apps leak keys.
There’s no separation. Your local dev, staging, and production all tend to share one file or one pattern, so a value that should differ per environment often doesn’t, and a test key sneaks into production or worse.
The goal isn’t to make secrets complicated. It’s to have one source of truth, injected at runtime, never committed, and rotatable in one place.
How to set up real secrets management
You don’t need HashiCorp Vault for a small app. Here’s a setup that’s a genuine upgrade and takes an afternoon.
Step 1: get secrets out of the repo and prove they’re gone.
Lock the .gitignore first:
# .gitignore
.env
.env.local
.env.*.local
*.pem
Then confirm nothing secret is already tracked, including in history:
# Anything sensitive currently tracked?
git ls-files | grep -E '\.env|\.pem' | grep -v '\.env\.example'
# Anything sensitive anywhere in history?
git log --all --full-history -p -- '.env' | head
If either prints real secrets, rotate those keys now. They’re already out.
Step 2: keep a committed template so people know what to fill in.
Commit a .env.example with keys and empty values. It documents what the app needs without exposing anything:
# .env.example (safe to commit)
DATABASE_URL=
STRIPE_SECRET_KEY=
OPENAI_API_KEY=
JWT_SIGNING_SECRET=
Step 3: read every secret from the environment, and fail loud if it’s missing.
Validate secrets at startup so a misconfiguration crashes immediately instead of failing weirdly in production. If you use TypeScript, do it once at the boundary:
import { z } from "zod";
const envSchema = z.object({
DATABASE_URL: z.string().url(),
STRIPE_SECRET_KEY: z.string().startsWith("sk_"),
OPENAI_API_KEY: z.string().min(1),
JWT_SIGNING_SECRET: z.string().min(32),
});
// Throws at startup if anything is missing or malformed
export const env = envSchema.parse(process.env);
Now a missing key is a clear crash on boot, not a silent 500 an hour into launch.
Step 4: inject secrets at runtime instead of storing a file on the server.
This is the real upgrade. Instead of a .env file sitting on the box, hold your secrets in one managed place and let it inject them into the process. Good options, from lightest to most robust:
- Your host’s secrets store. Most platforms have one built in (encrypted at rest, set once, injected as environment variables). If you’re on a managed host, use it. No file on disk, one place to edit.
- A dedicated tool like SOPS. SOPS encrypts a secrets file with a key (age or KMS) so you can commit the encrypted version safely and decrypt it only at deploy time. Good when you want secrets versioned in git without being readable.
- A managed secret store (AWS Secrets Manager, Doppler, Infisical). One source of truth, per-environment values, rotation, and an audit log of who accessed what.
For a self-hosted app on your own server, a clean pattern is: keep secrets in your host’s secret store or an encrypted SOPS file, inject them into the container’s environment at start, and never write a plaintext .env to disk. A Docker Compose service reads them from the environment, not a mounted file:
services:
app:
image: myapp:latest
environment:
DATABASE_URL: ${DATABASE_URL}
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY}
OPENAI_API_KEY: ${OPENAI_API_KEY}
# secrets come from the deploy environment, not a committed file
Step 5: separate environments and rotate on a schedule.
Use different keys for dev, staging, and production. A leaked dev key should never touch real customer data or a live Stripe account. And put a date on rotation: swapping keys every 90 days, plus immediately on any suspected exposure or team change, turns a leak from a disaster into an inconvenience because the exposed value is short-lived.
The whole point: one source of truth, injected at runtime, never on disk in plaintext, never in git, rotatable in one place. That’s the jump from “a file that works” to “a setup you can trust.”
Or, we do it for you
A production audit checks where your secrets actually live (repo, history, shipped bundle, server disk) and sets you up with runtime injection and a rotation plan, so no plaintext secret sits where it shouldn’t. If you want a second pair of eyes on your setup regardless of what you built it with, get a free production audit.
FAQ
Is a .env file always bad?
No. On your own machine for local development, a .env that’s properly git-ignored is fine and normal. It becomes a liability when it travels: copied to servers, pasted into chats, or committed to git. The fix is to stop treating a file as your production source of truth and inject secrets at runtime instead.
Do I need a full vault like HashiCorp Vault?
Almost certainly not for a small app. A vault is powerful and heavy. Your host’s built-in secrets store, or a lightweight tool like SOPS or Doppler, gets you the important wins (one source of truth, no plaintext on disk, rotation) without the operational weight. Reach for a full vault when you actually have the scale and team to justify it.
How often should I rotate secrets?
On a schedule of roughly every 90 days for routine hygiene, and immediately whenever a key might be exposed or a team member with access leaves. Frequent rotation is only painless if you have one place to change a value, which is exactly why the runtime-injection setup matters. Rotation you dread is rotation you skip.
What’s the difference between a secret and a config value?
A config value (a public URL, a feature flag, a port) is harmless if exposed. A secret (a password, an API key, a signing key) grants access or costs money if leaked. Manage them differently: config can live in plain committed files, secrets must be injected and kept out of the repo entirely.