Env and Secrets Without Leaking Production
Configuration layers, secret managers, and Next.js env conventions so dev convenience does not become a credential incident.
Environment variables are the duct tape of modern deployment—and the most common place production credentials leak into chat logs, screenshots, and client bundles. Application teams need a clear model: what is config, what is secret, what ships to the browser, and who rotates what when.
This post is for engineers shipping SaaS on Node or Next.js. It complements Docker for app developers (never bake .env into images), auth session patterns (cookie and signing secrets), and API design (service-to-service keys on server-only paths).
Config versus secrets
Config is non-sensitive and can live in repo examples: feature flag defaults, public API base URLs for staging, log levels. Secrets are credentials, signing keys, database URLs with passwords, and third-party API tokens. The Twelve-Factor App config principle—store config in the environment—still holds; the nuance is how you inject secrets without scattering them.
Document a single .env.example with dummy values and comments. Real .env files stay local and gitignored. CI and production pull from a secret manager or platform vault.
Next.js: server, client, and the NEXT_PUBLIC_ trap
Next.js inlines NEXT_PUBLIC_* variables into the client bundle at build time. Anything prefixed is visible to users who view source or network tabs. Never put database URLs, admin tokens, or webhook signing secrets behind NEXT_PUBLIC_.
Server-only variables are available in Server Components, Route Handlers, and getServerSideProps legacy paths without the prefix. Read Next.js environment variable documentation before adding a new key—naming mistakes are irreversible leaks once deployed.
For static export marketing sites, the build runs at CI time: only public vars belong in that pipeline. Server secrets for a separate API stay on the API host, not the static artifact bucket.
Layers: local, preview, staging, production
Use distinct credentials per environment. Sharing one Stripe key or one database between staging and prod guarantees eventual cross-contamination. Preview deployments from pull requests deserve isolated databases or scrubbed fixtures—not prod read replicas “for realism.”
Map environments explicitly in your deploy platform (Vercel, Fly, Railway, ECS task definitions). Avoid “prod secrets with NODE_ENV=development” hacks.
Secret managers and rotation
Platforms offer managed secrets: AWS Secrets Manager, GCP Secret Manager, Azure Key Vault, Doppler, 1Password Secrets Automation, and others. Pattern:
- Application reads secret at startup or on first use with caching.
- Deploy role can read; developers cannot read prod in daily workflows without break-glass.
- Rotation updates the manager; apps reload or restart on schedule.
Document rotation runbooks: who generates the new key, which services restart, dual-key windows for APIs that support two active keys.
.env files in Docker and compose
Compose env_file is convenient for local dev. Add .env to .dockerignore. In production, inject env from the orchestrator, not from files committed to the image. Docker secrets apply in Swarm; on Kubernetes use Secrets mounted as files or env from external operators—pick one pattern per cluster.
Logging and error reporting
Structured logs must redact tokens. Wrappers around fetch should not log full Authorization headers. Error trackers (Sentry, etc.) scrub PII and secrets when configured—verify scrubbing rules after adding new integrations.
Support staff pasting “full env dump” into tickets is a process problem; train alternatives (request id, user id, timestamp).
CI/CD secrets
GitHub Actions, GitLab CI, and others encrypt repository secrets. Scope secrets to environments (production only on protected branches). Fork PRs must not access prod secrets—use workflow rules.
Build logs sometimes echo env if scripts are careless (printenv debugging). Review CI scripts in code review like application code.
Third-party keys and least privilege
Issue API keys per service with minimal scopes: read-only where possible, IP allowlists when vendors support them. Separate keys for billing webhooks versus outbound API calls so a leak in one path does not own the account.
Pair webhook verification with secrets stored server-side only; background jobs and webhooks covers signature checks before parsing bodies.
Client-side OAuth and public identifiers
OAuth client IDs are often public; client secrets are not (except in confidential server flows). PKCE is standard for SPAs. Map redirect URIs explicitly in the provider console—wildcard prod redirects are a common misconfiguration.
Database URLs and connection strings
Treat DATABASE_URL as secret. Use connection pooling credentials distinct from migration superuser when your host allows. Postgres basics includes least-privilege runtime roles.
Supply chain and build-time secrets
Some CI pipelines need short-lived tokens to pull private packages or deploy. Use OIDC federation to cloud roles instead of long-lived access keys in GitHub secrets when your provider supports it—GitHub’s OIDC with cloud providers reduces static key sprawl.
Build-time secrets for private npm registries differ from runtime API keys; scope registry tokens to read-only package pull, not account admin.
Feature flags and non-secret toggles
Flags do not need vault storage unless they gate dangerous admin behavior. Still avoid putting unreleased product names in public flags if marketing cares about surprises—product policy, not cryptography.
Break-glass and auditing
Emergency prod access should be time-bound, logged, and rare. Break-glass credentials live outside daily .env files. After incidents, rotate everything that might have been exposed—assume compromise once a secret hit Slack.
Onboarding and offboarding
When someone leaves the team, rotate shared dev secrets they had access to—not because you distrust people, but because laptops and password managers outlive employment. Individual sandbox API keys beat one shared “team” Stripe test key in a wiki page.
New hires should get .env.example plus links to the secret manager break-glass process, not a zip of prod dumps “to get unblocked.”
Checklist before shipping a new env var
- Is it secret? If yes, server-only injection, never
NEXT_PUBLIC_. - Which environments need distinct values?
- Who rotates it and what restarts?
- Will it appear in logs, error reports, or client bundles?
- Is it documented in
.env.examplewith a placeholder? - Does mobile or desktop client code ever receive it—if so, redesign.
Template repositories should ship with secret scanning enabled (GitHub push protection, git-secrets hooks) so the first commit never contains a real key that history-scrubbing later cannot fully erase from forks.
Closing
Env and secrets discipline is boring until it is a breach postmortem. Separate config from credentials, keep secrets off clients and out of images, use per-environment values, and wire rotation before you need it at 2 a.m. Your future security review—and your users—depend on habits that are mostly naming, documentation, and refusing to “just paste prod URL in Vercel for a quick test.”
Schedule a quarterly secret inventory: list integrations, which env vars they use, and whether each still has an owner. Orphaned keys from deprecated features are how attackers find forgotten doors long after the feature shipped.
For local overrides, prefer direnv or shell-specific hooks that load .env.local only in trusted directories—reduces the chance someone copies a root .env into a gist while debugging “just one variable.”
When legal or customers request SOC2-style evidence, your env inventory becomes the source of truth for “who can access production credentials.” Start the spreadsheet before the auditor asks—retroactive archaeology across Slack is painful.