Complete "Setting Up CI/CD Pipelines for Continuous Deployment" first to unlock this lesson.Go to previous lesson

Introduction

When you ship an AI-generated web app, the code is only half the battle. The other half is everything the code talks to — API keys for inference, database connection strings, third-party service credentials, and feature flags. Mishandling these in production is the single most common cause of real outages and real security incidents.

This lesson covers the practical workflow for keeping secrets safe across three environments: local development, staging, and production. You will learn a single mental model that works whether you deploy on Vercel, Railway, Render, Fly.io, or a raw VPS.

The Three Environments

Every app you build lives in at least three places, and each place has different rules for secrets.

Local development happens on your laptop. You need fast iteration, so secrets live in a `.env` file that is git-ignored. You load them with a library like `dotenv`. This file is never committed and never leaves your machine.

Staging is a copy of production with fake data. It exists to catch bugs before users see them. Secrets are injected by the hosting service (Railway Variables, Vercel Environment Variables, etc.) and are scoped to that environment only.

Production is what real users hit. Secrets come from the same hosting service but live in a stricter namespace, often with separate access controls. A leaked staging key is annoying; a leaked production key is a billing disaster.

sơ đồ 3 môi trường dev/staging/prod cùng nguồn cấp secrets khác nhau

Loading Environment Variables in Code

The pattern below works for almost every Node.js or Python backend. The key idea: read once at startup, fail loudly if a required secret is missing.

javascript
import dotenv from 'dotenv';
dotenv.config();
const required = ['DATABASE_URL', 'OPENAI_API_KEY', 'JWT_SECRET'];
for (const key of required) {
if (!process.env[key]) {
throw new Error(`Missing required env var: ${key}`);
}
}
export const config = {
databaseUrl: process.env.DATABASE_URL,
openaiKey: process.env.OPENAI_API_KEY,
jwtSecret: process.env.JWT_SECRET,
nodeEnv: process.env.NODE_ENV || 'development',
port: Number(process.env.PORT) || 3000,
};

Notice the startup check. It is far better to crash at boot than to silently send `undefined` to your database client at 3am. The `dotenv` call only reads a file if it exists, which means the same code runs unchanged in production where the host injects variables directly.

For Python, the equivalent uses `pydantic-settings`:

python
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str
openai_api_key: str
jwt_secret: str
node_env: str = "development"
port: int = 3000
settings = Settings()

Pydantic will raise a clear validation error if any required field is missing, achieving the same fail-fast behavior.

Database Connection Patterns

For a Postgres database on a managed service like Neon, Supabase, or Railway, the connection string is your secret. Treat it like a password.

javascript
import pg from 'pg';
import { config } from './config.js';
export const pool = new pg.Pool({
connectionString: config.databaseUrl,
max: 10,
idleTimeoutMillis: 30000,
ssl: config.nodeEnv === 'production' ? { rejectUnauthorized: true } : false,
});

Two things matter here. First, connection pooling prevents your app from opening a new TCP connection on every request. Second, SSL is required in production. Many managed databases reject unencrypted connections, but being explicit protects you if the default changes.

For serverless platforms (Vercel Functions, Cloudflare Workers), use a connection pooler that lives outside the function — Neon has one built in, Supabase has the `pgbouncer` transaction mode. The pool object above lives in process memory, which on serverless means a fresh pool per cold start, which is slow and can exhaust database connections under traffic spikes.

Managing API Keys for AI Services

AI APIs (OpenAI, Anthropic, Google, etc.) bill per token. A leaked key can drain a credit card in hours. Three rules keep you safe.

First, set a hard spend limit in the provider dashboard. Every major provider offers this. Second, create separate keys per environment — `myapp-dev`, `myapp-staging`, `myapp-prod` — so you can revoke one without touching the others. Third, never log the key, and never include it in error messages that get sent to a logging service.

javascript
const openaiHeaders = {
'Authorization': `Bearer ${config.openaiKey}`,
'Content-Type': 'application/json',
};
async function callOpenAI(messages) {
const res = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: openaiHeaders,
body: JSON.stringify({ model: 'gpt-4o-mini', messages }),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`OpenAI error ${res.status}: ${text}`);
}
return res.json();
}

The error handler captures the response body for debugging, which is fine because it does not contain the key — only the upstream provider's error. If you ever need to log the request headers for debugging, redact the `Authorization` field first.

Common Mistakes and How to Avoid Them

The most frequent mistake is committing a `.env` file. Even once. Git history is forever, and bots scan GitHub for exposed keys within minutes of a push. Add `.env` to `.gitignore` on day one, and use tools like `git-secrets` or `gitleaks` in CI to block accidental commits.

The second mistake is using the same database for development and production. It feels convenient until a `DROP TABLE` in a local script wipes production. Always use separate databases, and ideally separate provider projects, for each environment.

The third mistake is rotating keys without a plan. When you rotate a database password, every running instance of your app will fail to reconnect until it restarts. On platforms that support it, use connection strings that include the password and design your app to retry on startup. On platforms like Vercel, a redeploy after rotating the secret handles this automatically.

Summary

Treat every secret as if it will leak tomorrow — because eventually it will. Use `.env` files locally with `dotenv` or `pydantic-settings`, let your hosting platform inject variables in staging and production, validate required secrets at startup, pool database connections with `ssl: true` in production, and give every AI API key a spend limit plus a unique key per environment. This setup takes one afternoon to wire up and saves you from the most common category of production incident.

Lesson practice

Lesson Checkpoint

Review what you learned and get feedback on your work.

0/1
Answered · 0/1