Build the mental model
Hardcoding a database connection string or API key directly into source code works at first, but breaks down the moment that code is shared, committed to git, or deployed somewhere else.
Environment variables keep configuration outside the code entirely. A value like DATABASE_URL is set in the running environment, and code simply reads it at runtime through process.env in JavaScript or os.environ in Python.
The same codebase behaves differently depending on where it runs — dev might point at localhost with debugging on, prod points the same variable at a live domain with debugging off. Only the surrounding configuration changes.
- API keys and access tokens
- Database passwords and connection strings
- Signing keys and authentication secrets
- Environment Variable
- A named configuration value set outside the code, in the environment the application runs in, and read by the code at runtime.
- Secret
- A sensitive environment variable — an API key, password, or token — that must never be visible to anyone or anything without a legitimate need for it.
CODE + ENVIRONMENT CONFIG -> RUNNING APP
----------------------------------------
+------------------+
Dev Config -->| |--> App: localhost DB,
| Same Code | debug ON
| |
Prod Config-->| |--> App: prod DB,
+------------------+ debug OFFConnect it to a real scenario
For each piece of configuration, decide whether it belongs on the server only or is safe to reach the browser. Credentials and signing secrets stay server-side; a public API URL or feature flag needs an explicit public-safe prefix before a framework will expose it to client code.
Local development
Keep values in a .env file, and exclude it from version control with .gitignore so secrets never get committed by accident.
Production
Values live in your deployment platform's environment variable settings instead of any file at all.
If a secret is ever accidentally committed to git, rotating it everywhere it's used is not optional — git history keeps old commits indefinitely, so deleting the line in a new commit does not remove it from the project's history.
Never expose server-side secrets to frontend code
Anything that ends up inside client-side JavaScript is downloaded to every visitor's browser and readable by anyone who opens developer tools — no matter how it was built or minified. If a variable lacks your framework's public-safe prefix (or gets imported into a component that ships to the browser), treat it as instantly public. A database password, private API key, or signing secret in frontend code is not hidden — it is published.
Try the working example
function getConfig(env) {
return {
databaseUrl: env.DATABASE_URL || "postgres://localhost:5432/dev_db",
appUrl: env.APP_URL || "http://localhost:3000",
};
}
const devEnv = {};
const prodEnv = {
DATABASE_URL: "postgres://prod-db.internal:5432/app",
APP_URL: "https://myapp.com",
};
console.log("Dev config:", getConfig(devEnv));
console.log("Prod config:", getConfig(prodEnv));Dev config: { databaseUrl: 'postgres://localhost:5432/dev_db', appUrl: 'http://localhost:3000' }
Prod config: { databaseUrl: 'postgres://prod-db.internal:5432/app', appUrl: 'https://myapp.com' }5-minute try-it
Add a THIRD env state to the code example representing a 'staging' environment with its own DATABASE_URL, and confirm getConfig() returns the right values without touching the function itself — only the environment object changes.
One important caution
Committing a .env file to git, which puts every secret it holds into permanent, recoverable history.
Assuming a variable is safe just because it's read from process.env — if it lacks a public-safe prefix and still ends up bundled into frontend code, it's exposed.
Where should this password live?
The Twelve-Factor App — III. Config — Cloud & Deployment