Keep secrets out of code
Read secrets from env, fail fast if one is missing, never commit .env.
From security-best-practices: Step 4, Manage secrets.
Find the hardcoded secrets
Edit the code; the right side reads from env.
HardcodedBad
Read from envGood
// src/lib/config.ts
function required(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`${name} environment variable is required`);
return value;
}
export const config = {
databaseUrl: required("DATABASE_URL"),
stripeSecretKey: required("STRIPE_SECRET_KEY"),
sendgridApiKey: required("SENDGRID_API_KEY"),
};
Keep .env out of the commit
Edit .gitignore until only the template ships, then commit.
.gitignoreEdit
.env*!.env.examplegit add -ABlocked
# .gitignore
.env
.env.*
!.env.example
# Already committed? Untrack it, then rotate the key.
# git rm --cached .env
Boot with a missing variable
Set the variables, boot both, then send a request.
Read when usedBad
Checked at startupGood
// before app.listen()
const dbUrl = process.env.DATABASE_URL;
if (!dbUrl) {
throw new Error("DATABASE_URL environment variable is required");
}
const jwtSecret = process.env.JWT_SECRET ?? "";
if (jwtSecret.length < 32) {
throw new Error(`JWT_SECRET must be at least 32 characters (got ${jwtSecret.length})`);
}
// generate one: openssl rand -base64 32
# Kubernetes Secret
apiVersion: v1
kind: Secret
metadata:
name: myapp-secrets
type: Opaque
stringData:
database-url: postgresql://user:password@postgres:5432/mydb
jwt-secret: your-jwt-secret
Why it matters, pros and cons
Why it matters
A key in source lives in every clone, and in history forever.
Pros
- One build, different values per environment.
- Rotating a key is a restart, not a commit.
- A bad deploy fails; the old release keeps serving.
Cons
- Env vars still leak via crash dumps and child processes.
NEXT_PUBLIC_andVITE_ship values to the browser.- Regex scanners miss custom formats; add push protection.