Proper secrets management prevents data breaches. This guide covers handling configuration and sensitive data securely.
Environment Variables Basics#
Loading Environment Variables#
1// .env file (never commit!)
2DATABASE_URL=postgresql://user:pass@localhost:5432/db
3API_KEY=sk_live_abc123
4NODE_ENV=development
5
6// Load with dotenv
7import 'dotenv/config';
8
9// Or manually
10import dotenv from 'dotenv';
11dotenv.config({ path: '.env.local' });
12
13// Access variables
14const dbUrl = process.env.DATABASE_URL;Type-Safe Environment Variables#
1// env.ts
2import { z } from 'zod';
3
4const envSchema = z.object({
5 NODE_ENV: z.enum(['development', 'production', 'test']),
6 DATABASE_URL: z.string().url(),
7 API_KEY: z.string().min(1),
8 PORT: z.coerce.number().default(3000),
9 ENABLE_LOGGING: z.coerce.boolean().default(true),
10});
11
12export const env = envSchema.parse(process.env);
13
14// Usage - fully typed!
15console.log(env.PORT); // number
16console.log(env.NODE_ENV); // 'development' | 'production' | 'test'Next.js Environment Variables#
1# .env.local (gitignored)
2DATABASE_URL=postgresql://...
3SECRET_KEY=abc123
4
5# .env (can be committed)
6NEXT_PUBLIC_API_URL=https://api.example.com
7
8# Access
9# Server-side: process.env.DATABASE_URL
10# Client-side: process.env.NEXT_PUBLIC_API_URL (NEXT_PUBLIC_ prefix required)Secrets Managers#
AWS Secrets Manager#
1import { SecretsManager } from '@aws-sdk/client-secrets-manager';
2
3const client = new SecretsManager({ region: 'us-east-1' });
4
5async function getSecret(secretName: string): Promise<Record<string, string>> {
6 const response = await client.getSecretValue({ SecretId: secretName });
7
8 if (response.SecretString) {
9 return JSON.parse(response.SecretString);
10 }
11
12 throw new Error('Secret not found');
13}
14
15// Usage
16const dbCredentials = await getSecret('prod/database');
17const connectionString = `postgresql://${dbCredentials.username}:${dbCredentials.password}@${dbCredentials.host}:5432/mydb`;HashiCorp Vault#
1import Vault from 'node-vault';
2
3const vault = Vault({
4 endpoint: process.env.VAULT_ADDR,
5 token: process.env.VAULT_TOKEN,
6});
7
8async function getSecret(path: string) {
9 const { data } = await vault.read(`secret/data/${path}`);
10 return data.data;
11}
12
13// Usage
14const apiKeys = await getSecret('api-keys');
15console.log(apiKeys.stripe_key);Google Secret Manager#
1import { SecretManagerServiceClient } from '@google-cloud/secret-manager';
2
3const client = new SecretManagerServiceClient();
4
5async function getSecret(name: string): Promise<string> {
6 const [version] = await client.accessSecretVersion({
7 name: `projects/my-project/secrets/${name}/versions/latest`,
8 });
9
10 return version.payload?.data?.toString() || '';
11}CI/CD Secrets#
GitHub Actions#
1# .github/workflows/deploy.yml
2jobs:
3 deploy:
4 runs-on: ubuntu-latest
5 steps:
6 - uses: actions/checkout@v4
7
8 - name: Deploy
9 env:
10 DATABASE_URL: ${{ secrets.DATABASE_URL }}
11 API_KEY: ${{ secrets.API_KEY }}
12 run: |
13 npm run deployEnvironment-Specific Secrets#
1jobs:
2 deploy:
3 runs-on: ubuntu-latest
4 environment: production # Links to GitHub environment
5 steps:
6 - name: Deploy
7 env:
8 # Secrets from 'production' environment
9 DATABASE_URL: ${{ secrets.DATABASE_URL }}
10 run: npm run deployDocker Secrets#
Docker Compose#
1# docker-compose.yml
2services:
3 app:
4 image: myapp
5 secrets:
6 - db_password
7 - api_key
8 environment:
9 DB_PASSWORD_FILE: /run/secrets/db_password
10
11secrets:
12 db_password:
13 file: ./secrets/db_password.txt
14 api_key:
15 external: true # Created via `docker secret create`Reading Secret Files#
1import fs from 'fs';
2
3function getDockerSecret(name: string): string {
4 const secretPath = `/run/secrets/${name}`;
5
6 if (fs.existsSync(secretPath)) {
7 return fs.readFileSync(secretPath, 'utf8').trim();
8 }
9
10 // Fallback to environment variable
11 return process.env[name.toUpperCase()] || '';
12}
13
14const dbPassword = getDockerSecret('db_password');Kubernetes Secrets#
1# secret.yaml
2apiVersion: v1
3kind: Secret
4metadata:
5 name: app-secrets
6type: Opaque
7stringData:
8 DATABASE_URL: postgresql://user:pass@host:5432/db
9 API_KEY: sk_live_abc123
10
11---
12# deployment.yaml
13apiVersion: apps/v1
14kind: Deployment
15spec:
16 template:
17 spec:
18 containers:
19 - name: app
20 envFrom:
21 - secretRef:
22 name: app-secretsBest Practices#
Never Commit Secrets#
1# .gitignore
2.env
3.env.local
4.env.*.local
5*.pem
6*.key
7secrets/Use Different Secrets Per Environment#
1# Development
2.env.development
3
4# Staging
5.env.staging
6
7# Production
8.env.production # Or use secrets managerRotate Secrets Regularly#
1// Support multiple API keys during rotation
2const API_KEYS = [
3 process.env.API_KEY_NEW,
4 process.env.API_KEY_OLD,
5].filter(Boolean);
6
7function validateApiKey(key: string): boolean {
8 return API_KEYS.includes(key);
9}Audit Secret Access#
1async function getSecret(name: string, reason: string) {
2 logger.info({
3 action: 'secret_access',
4 secretName: name,
5 reason,
6 timestamp: new Date().toISOString(),
7 });
8
9 return secretsManager.getSecret(name);
10}What NOT to Do#
1// ❌ Never hardcode secrets
2const API_KEY = 'sk_live_abc123';
3
4// ❌ Never log secrets
5console.log('API Key:', process.env.API_KEY);
6
7// ❌ Never expose in error messages
8throw new Error(`Failed with key: ${apiKey}`);
9
10// ❌ Never commit .env files
11// ❌ Never store in client-side codeConclusion#
Use environment variables for configuration, secrets managers for production secrets, and never commit sensitive data. Implement type-safe validation, rotate credentials regularly, and audit access to sensitive data.