Secrets management: where credentials should live and how they should rotate
The question is not whether a secret will leak. It is whether you will know, and how long it takes to make the leaked one useless.
Configuration should be validated against a schema at application startup so a missing or malformed variable crashes immediately with a clear message. Secrets belong in a secrets manager injected at runtime, non-secret settings can live in version-controlled config, and no secret should ever reach a client bundle.
Because a missing variable discovered at request time produces a confusing runtime error far from its cause.
import { z } from "zod";
const EnvSchema = z.object({
NODE_ENV: z.enum(["development", "test", "production"]),
DATABASE_URL: z.string().url(),
REDIS_URL: z.string().url(),
SESSION_SECRET: z.string().min(32),
STRIPE_SECRET_KEY: z.string().startsWith("sk_"),
PORT: z.coerce.number().int().positive().default(3000),
});
// Throws at import time, before the server accepts a single request
export const env = EnvSchema.parse(process.env);Without this, a missing DATABASE_URL surfaces as 'connect ECONNREFUSED undefined:undefined' during the first request that needs the database — possibly hours after deployment, in a code path unrelated to configuration.
| Settings | Secrets | |
|---|---|---|
| Examples | Feature flags, log level, page size | API keys, database passwords, tokens |
| Storage | Version-controlled config or env | Secrets manager |
| Rotation | Rarely | On a schedule and on incident |
| Visibility | Fine for the whole team | Least privilege |
| In a repository | Acceptable | Never |
| Audit trail | Git history | Access log in the manager |
Conflating them is why secrets end up in repositories. Once a secret is committed, rewriting history is not enough — it must be rotated, because it may already have been cloned or scraped.
Environment variables are a poor feature flag mechanism: changing one requires a deploy, and the value is the same for every user. That is fine for a build-time toggle and wrong for anything you want to roll out gradually.
Never with real values. Commit a .env.example listing every required variable with placeholders and a short comment, and add .env to .gitignore. New developers then know exactly what to provide.
Rotate it immediately — that is the only reliable remediation. Removing it from history is worth doing afterwards, but assume the value is compromised the moment it was pushed.
A managed secrets service — AWS Secrets Manager, Google Secret Manager, Vault, or your platform's encrypted environment settings — injected at runtime rather than baked into an image.
Development, staging and production covers most teams. Preview environments per pull request are extremely useful and largely automatic on modern platforms. More than that usually adds maintenance rather than safety.
ROVQIX Engineering
Engineering team, ROVQIX
The ROVQIX engineering team builds and maintains web platforms, APIs and infrastructure for clients across SaaS, ecommerce and enterprise. These notes come out of real production work — deploys, incidents, migrations and audits.
ROVQIXdesigns and builds production web platforms — Next.js front ends, Node.js APIs and the infrastructure behind them. Tell us what you're building and we'll scope it with you.
The question is not whether a secret will leak. It is whether you will know, and how long it takes to make the leaked one useless.
A pipeline that takes 25 minutes and fails randomly does not improve quality. It teaches the team to merge on red.
A 1.2GB Node image that runs as root and ignores SIGTERM is the default outcome. Every part of that is avoidable.
No spam. Just the occasional case study and craft breakdown.