
Env Files and Secrets Without Leaking Keys. A Build Next Stack field guide for learners shipping small projects.
Three-file pattern: example, local, ignored
Secrets management for solo builders starts boring: .env.example committed, .env gitignored, .env.production never on disk in repo—supplied by host dashboard or secret manager. Sam names keys identically across environments so code reads one variable name everywhere.
.env.example holds fake values and comments:
- STRIPE_SECRET_KEY=sk_test_REPLACE_ME — Stripe dashboard → Developers → API keys
- APP_BASE_URL=http://localhost:3000 — prod: https://yourdomain.com
New machine setup is cp .env.example .env and replace three lines—not archaeology in old laptop backups.
Load order and surprises in frameworks
Frameworks load env differently—Next.js, Vite, Django, Flask each have prefix rules and override order. Sam documents load order in README in one paragraph: Vite exposes only VITE_* to client; never put secrets there. That sentence prevented a public API_KEY in a bundled JS file once.
Rule: if a variable touches the browser bundle, treat it as public. Rotate anything that ever lived in a client-prefixed file by mistake.
Validate required env at boot with a tiny script that lists missing keys and exits non-zero. Fail fast at startup beats cryptic 500s on first request.
Pre-commit hooks that catch obvious leaks
Sam runs gitleaks or a simple grep hook on staged files:
- Block commits containing AKIA AWS patterns or sk_live_ Stripe keys
- Block adding .env unless filename is .env.example
- Warn on high-entropy strings in JSON configs
Hooks are not foolproof; they catch tired mistakes. Install once per machine, share config in repo .gitleaks.toml so clones inherit protection.
If a hook blocks a false positive, fix the config—do not bypass with –no-verify unless you enjoy rotation weekends.
Rotating after a scare—or before
Assume rotation will happen. Store provider notes: where to revoke old keys, which env vars update, which deploy restart required. Sam keeps a private note titled Rotation runbook with links—not in repo.
After any leak suspicion: revoke immediately, generate new key, update host env, redeploy, then hunt git history. Public GitHub means bots scan within minutes; private repos still get pushed eventually.
Schedule rotation for long-lived tokens every six months calendar reminder—low urgency until it saves you during a laptop theft story.
Production secrets on the host, not in artifacts
Build pipelines should not bake secrets into Docker images or static zips. Inject at runtime: platform env vars, mounted secrets, or OIDC to cloud APIs. Sam’s static site needs no secrets; API runs on Fly.io with secrets set via CLI, never in Dockerfile ENV.
Downloaded build logs and error reports must scrub tokens. Wrap logging helpers that redact Authorization headers and known env var values before stringify.
Sharing with collaborators without Slack dumps
When a friend helps debug, send secrets through a one-time link tool or password manager share—not Discord paste. Time-box access; revoke after the session. Separate dev keys from prod keys so helpers never touch live data.
For open source repos, issue templates remind contributors: never commit secrets; use .env.example only. Template text is cheaper than incident response.
Local dev realism: fake services
Reduce secret count by faking providers locally—Stripe test mode, Mailhog for email, MinIO for S3-shaped storage. Sam runs integration tests against fakes so CI needs fewer real credentials.
Document which features degrade without keys: search disabled if ALGOLIA_APP_ID empty. Graceful degradation beats mysterious crashes when a optional key is missing.
Every fake service port and startup command lives in README quick start—same place as real env docs.
Sam logs env-related incidents in a private note—2026-04 leaked test key in screenshot—to remember why hooks exist. Shame as documentation beats repeating mistakes.
Twelve-factor habits without the manifesto homework
Solo projects still benefit from two twelve-factor ideas: config in environment, and dev/prod parity for the pieces you actually run. Sam does not containerize for sport— but if production uses Postgres, local uses Postgres in Docker instead of SQLite pretending to be compatible.
Parity failures show up as works on my machine migrations and timezone bugs. Match major versions: if prod is Node 20, CI and local are Node 20—not 18 because nvm default lagged.
Config drift checklist before deploy: diff .env.example keys against host dashboard keys. Missing key deploys are silent until first user hits the broken path.
Incident template when a key leaks anyway
Sam keeps a one-page incident template offline:
- Revoke compromised credential at provider—timestamp noted
- Rotate dependent keys if blast radius unclear
- Search git history and forks; open provider support ticket if public
- Update host env; redeploy; verify logs clean
- Postmortem: how it entered repo; hook or process fix same day
Panic skips steps; template is order under stress. Solo does not mean skipping postmortem—a five-line note prevents second leak from same path.
Practice once with a fake key in a scratch repo so muscle memory exists before real stakes.
Sam adds SECRET_SCAN=1 to CI optional job running gitleaks on PR diffs—cheap second line after pre-commit hooks that laptops skip when cloned fresh.
Naming conventions that prevent wrong-file commits
Never name real secrets file secrets.json in repo root—too easy to commit. Sam uses .env only locally and config/secrets.example.json for shape docs. Production secrets live solely in host UI with names matching code constants exactly—typo in dashboard wastes hours.
When adding a new integration, create example entry first, then code that reads it, then local value last. Order prevents code that reads undefined vars without docs.
Review .gitignore when adding new secret file patterns—team of one still forgets after generating .pem for TLS experiments.
Audit tonight: example file and gitignore
Open your side project root. Confirm .env is ignored, .env.example lists every key your code reads, and no secret appears in client bundles.
Add a pre-commit leak scan this week—it takes fifteen minutes once and pays off on the night you paste the wrong key into a config.
Production secrets belong on the host at runtime, not in git history. Rotate calmly because you wrote the runbook before panic.