Expert · Deploying and running the app · topic 25 of 34 · 30 min
Environment variables and secrets
Explanation
Code is the same, config changes
Config (configuration) is everything that changes between places where the app runs: the database address, the port, secret keys, the website allowed to call the API. The Twelve-Factor App, a well-known list of rules for apps that run in the cloud, says: keep config in environment variables, not in the code.
An environment variable is a name and a value (like PORT=3000) that the operating system or the host hands to your program when it starts. In Node.js you read it with process.env.PORT. The value is always a string, so "3000" must be turned into the number 3000 yourself.
On your laptop: .env and .env.example
# .env.example (committed: names and fake values only)
NODE_ENV=development
PORT=3000
# PostgreSQL connection string for your local database
DATABASE_URL=postgres://notes:notes@localhost:5432/notes_dev
# Long random string used to sign session cookies
SESSION_SECRET=change-me-to-a-long-random-string
# The frontend address allowed by CORS
CORS_ORIGIN=http://localhost:5173The real values go in a file named .env next to your code. Add .env to .gitignore so git never commits it. Commit .env.example instead: it lists every variable the app needs, with fake values and a comment, so a new teammate knows what to fill in. Node.js 20.6 and later can load the file with node --env-file=.env server.js; older projects use the dotenv package.
One set of values per environment
| Variable | development (laptop) | preview (each pull request) | production (real users) |
|---|---|---|---|
| NODE_ENV | development | production | production |
| DATABASE_URL | local database | a separate preview database | the production database |
| SESSION_SECRET | any test string | its own random value | its own long random value |
| CORS_ORIGIN | http://localhost:5173 | the preview URL | https://notesapp.in |
On the server there is no .env file to copy around. You type each secret into the host's dashboard (for example the Environment page of a Render service or the Environment Variables settings of a Vercel project), or into GitHub Actions secrets for a pipeline. The host stores them encrypted and gives them to the app when it starts.
Frontend variables are public
The React frontend runs in the user's browser. Vite only lets code read variables whose names start with VITE_ (through import.meta.env), and Next.js does the same for names starting with NEXT_PUBLIC_. At build time their values are pasted into the JavaScript files. Anyone can open DevTools and read them. They are also fixed at build time, so changing one needs a new build.
Check the config when the app starts
function loadConfig(env) {
const missing = ['DATABASE_URL', 'SESSION_SECRET'].filter((k) => !env[k]);
if (missing.length) {
throw new Error('Missing required env: ' + missing.join(', '));
}
return {
port: env.PORT ? Number(env.PORT) : 3000,
databaseUrl: env.DATABASE_URL,
sessionSecret: env.SESSION_SECRET,
};
}
const config = loadConfig(process.env); // crashes at start if something is missingThis is called failing fast: if a value is missing, the app stops at once with a clear message in the deploy log. Without the check, the app starts "fine" and crashes hours later on the first database query, with a confusing error. Libraries such as zod or envalid can do these checks for you.
When a secret leaks
- Rotate it: create a new key at the provider, put it in the host dashboard and redeploy.
- Revoke the old key so it stops working everywhere.
- Check the provider's logs or bills for anything done with the old key.
- Remove the secret from the code. Deleting the file is not enough, because git history still holds it; the rotation is the real fix.
Watch the lesson
Environment variables and secrets · 1/7
The same Notes code runs on your laptop and in production. Only the settings change, and those settings are called config.
Prefer a video by a teacher? Find free videos on “environment variables secrets node js vite production explained” on YouTube ↗
In real life: The payment key in the frontend
A student team built an event ticket site. To make payments work quickly, someone put the payment gateway's secret key in a VITE_ variable. The site worked. A week later a visitor opened DevTools, found the key in the JavaScript file and could have made refunds and read payment data. The team rotated the key the same day, revoked the old one, moved the payment call into the API (which reads the key from process.env on the server) and kept only the public key id in the frontend.
Practice (0/3 done)
Write loadConfig(env). DATABASE_URL and SESSION_SECRET are required: if any is missing or empty, throw an Error whose message is "Missing required env: " followed by the missing names joined with ", " (in that order). PORT is optional (default 3000) and must become a number; if it is given but is not a whole number above 0, throw an Error. Return { nodeEnv, port, databaseUrl, sessionSecret, corsOrigin } in that key order, where nodeEnv defaults to "development" and corsOrigin to "http://localhost:5173".
Write findLeakyPublicVars(env) that returns the names of public frontend variables that look secret, sorted A to Z. A variable is public if its name starts with "VITE_" or "NEXT_PUBLIC_". It looks secret if its name contains SECRET, PASSWORD, TOKEN, PRIVATE or DATABASE, or its value starts with "sk_live_", "sk_test_", "postgres://" or "postgresql://". Non-public variables are never reported.
Write the .env.example files for the Notes API and the Notes frontend, and a short table that says, for every variable, whether it is secret, its value in each environment (development, preview, production) and where it is stored.
Quick quiz
Read more (free)
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.