Back to Blog
Environment VariablesSecretsSecurityConfiguration

Environment Variables and Secrets Management

Manage configuration and secrets securely. Learn environment variables, secret managers, and best practices for sensitive data.

B
Bootspring Team
Engineering
February 27, 2026
4 min read

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 deploy

Environment-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 deploy

Docker 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-secrets

Best 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 manager

Rotate 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 code

Conclusion#

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.

Share this article

Help spread the word about Bootspring