Skip to content

Expert · Deploying and running the app · topic 25 of 34 · 30 min

Environment variables and secrets

Explanation

The same code runs on your laptop, on a preview copy and in production; only its settings change. Those settings, like the database address and secret keys, come from environment variables, never from the code. Secrets live in a local .env file (never committed) and in the host's dashboard, and the app checks them when it starts.

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.

Think of it like this: A tiffin service cooks from the same recipe every day. The order slip on the counter changes: how many tiffins, which building, the gate code. Anyone may see the recipe, but the slip with the gate code stays private. Your code is the recipe; environment variables are the order slip.

On your laptop: .env and .env.example

bash
# .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:5173
Every developer copies .env.example to .env and puts in their own real values.

The 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

Variabledevelopment (laptop)preview (each pull request)production (real users)
NODE_ENVdevelopmentproductionproduction
DATABASE_URLlocal databasea separate preview databasethe production database
SESSION_SECRETany test stringits own random valueits own long random value
CORS_ORIGINhttp://localhost:5173the preview URLhttps://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.

Careful: Never put a secret in a VITE_ or NEXT_PUBLIC_ variable: no database URL, no payment secret key, no session secret, no admin token. Fine examples are the API address or a public analytics id. If the frontend needs something secret, the API should do that work and return only the result.

Check the config when the app starts

javascript
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 missing
One place reads process.env, checks it and gives the rest of the app a clean config object.

This 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

  1. Rotate it: create a new key at the provider, put it in the host dashboard and redeploy.
  2. Revoke the old key so it stops working everywhere.
  3. Check the provider's logs or bills for anything done with the old key.
  4. Remove the secret from the code. Deleting the file is not enough, because git history still holds it; the rotation is the real fix.
Tip: Turn on GitHub secret scanning (and push protection where your plan offers it). It can warn you, or block the push, when a commit contains a known kind of key.

Watch the lesson

Environment variables and secrets · 1/7

Same code Different config

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)

Task 1 · Code · Javascript

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".

Task 2 · Code · Javascript

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.

Task 3 · Practice

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

Question 1

1.Which file should be committed to git?

Question 2

2.A developer adds VITE_DATABASE_URL to the React app. What is the problem?

Question 3

3.process.env.PORT is "8080". What type is it?

Question 4

4.Why should the API check its config when it starts?

Question 5

5.A session secret was pushed to a public GitHub repo. What fixes the problem?

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.

← Previous: Where to deploy