Config Lives in the Environment
On your laptop, Sprout talks to a local database with a throwaway password. On the live server it must use the real database and a real secret. The code has to be identical in both places, so the difference must come from outside the code. On Linux, that outside is the process's environment.
The rule
Same code everywhere; the environment says where it is running.
Every Linux process carries a set of named text values, its environment variables, inherited from whatever started it. Node exposes them as process.env:
const port = Number(process.env.PORT ?? 3000);
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
const secret = process.env.JWT_SECRET;
if (!secret) throw new Error('JWT_SECRET is not set');Reading config this way lets one build run anywhere. Failing loudly at startup when a required value is missing beats failing mysteriously on the first login.
In a Linux shell, export puts a variable into the shell's environment, and every program started from that shell afterwards inherits it:
export DATABASE_URL="postgres://sprout:devpass@localhost:5432/sprout"
printenv DATABASE_URL
node src/index.jsLocally, typing exports gets old, so the values live in a .env file loaded at startup, with node --env-file=.env src/index.js (Node 20.6 and later) or the dotenv package. .env is listed in .gitignore. Instead, commit a .env.example with the variable names and fake values, so a new teammate knows what to fill in.
On the server, the service manager starts the app and hands it the variables, for example from a systemd line such as EnvironmentFile=/etc/sprout/api.env. That file is readable only by its owner (chmod 600) and never enters git.
In the client, be careful. Vite lets client code use variables whose names start with VITE_, and at build time it writes their values straight into the JavaScript bundle, which anyone can read. Client variables are for public values only, such as a support email address. Secrets exist only in the server's environment.
The shortcut
For each config value ask: "Would I be happy to print this on a billboard?" No: it lives only in the server's environment, never in git and never in the client bundle. Yes: it may be a client variable or a committed default.
DATABASE_URL
It contains the database password. Server environment only:
.envlocally, the environment file on the server.
The classic slip is committing .env once, "just for now". Deleting it later does not help, because the value stays in git history. If a secret was ever committed, treat it as leaked and replace it.
Drills
Answer fast — trust the rule.
Where does a Node program read its environment variables?
Which file belongs in git?
A developer adds VITE_JWT_SECRET so the client can check tokens itself. What is wrong?
After export PORT=8080 in a shell, which processes see PORT?
Remember
- Same code everywhere; the environment says where it is running.
- Shortcut: "Would I print it on a billboard?" No means server environment only.
- Trap: committing
.envor putting secrets inVITE_variables. A committed secret is a leaked secret.
