An API key pushed by accident to a public repository, a database password hard-coded in the source, a “public” variable that ends up in the JavaScript bundle shipped to the browser: most secret leaks are not the result of a sophisticated attack, but of misplaced configuration. Separating configuration from code remains one of the highest-return habits in web development — and one of the most poorly tooled in projects that grow quickly.
Why configuration belongs outside the code
The principle is old and fits in one sentence: the code describes what the application does, the configuration describes where and how it runs. A single codebase should start locally, in staging and in production without a single line changing — only the environment values differ. This is the recommendation formalised by the Twelve-Factor App methodology, and it has a direct security consequence: what lives in the environment does not live in the Git history.
The useful distinction is not “secret” versus “non-secret” but “what changes per environment”. A staging API URL is not confidential, yet it belongs in configuration just as much as an access token. Hardening a server begins with the same reflex: no credential in cleartext inside a versioned file.
The .env file and the .gitignore rule
The dominant convention is a .env file at the project root: a list of KEY=value pairs, one per line. Its strength is simplicity; its trap is that committing it by accident is trivial. The first line to write in a project is not a variable, it is the exclusion of the file:
# .gitignore — à faire AVANT le premier commit
.env
.env.*
!.env.exampleThe file actually versioned is a .env.example that documents the expected keys without their values. A new team member copies it to .env and fills it in. A typical version looks like this:
# .env.example — versionné, sans secrets
APP_ENV=local
APP_DEBUG=true
DATABASE_URL=
STRIPE_SECRET_KEY=
MAIL_DSN=The prefix that exposes to the browser. Front-end build tools (Vite, Next.js) inject into client code only the variables carrying an agreed prefix: VITE_ for Vite, NEXT_PUBLIC_ for Next.js. Anything with that prefix is public: it ends up readable in the bundle downloaded by every visitor. A secret key must never carry that prefix, or it will be served to the whole world.
Loading variables per stack
Each ecosystem has its own way of reading the environment. The three most common on the web look alike in intent but differ in the details.
On the PHP side, Laravel reads the .env automatically at boot and exposes values through the env() helper. One point deserves attention: once configuration is cached in production (php artisan config:cache), env() returns nothing outside the configuration files. The value must therefore be read via config(), not env(), everywhere else in the application:
// config/services.php
return [
'stripe' => [
'secret' => env('STRIPE_SECRET_KEY'),
],
];
// Dans le code applicatif : config(), jamais env()
$key = config('services.stripe.secret');On the Node.js side, reading a .env file no longer requires an external dependency: the native --env-file flag is enough, and the values land in process.env. The same mechanism carries over to a production deployment.
# Chargement natif, sans paquet supplémentaire
node --env-file=.env server.js// server.js — les valeurs sont dans process.env
const dsn = process.env.DATABASE_URL;
if (!dsn) {
throw new Error("DATABASE_URL manquante : vérifier le fichier .env");
}On the front-end build side, Vite exposes to the client only the prefixed subset and makes it available through import.meta.env. The boundary is clear: the server sees everything, the browser sees only the public prefix.
// Accessible dans le navigateur — donc PUBLIC
const apiBase = import.meta.env.VITE_API_URL;Comparing storage methods
The .env file covers the developer workstation. It shows its limits as soon as secrets must be shared across a team or rotated regularly. The table below places each approach.
| Method | Scope | Sensitive secrets | Team sharing |
|---|---|---|---|
.env file | Local machine, one project | Acceptable if kept out of the repo | Manual, fragile |
| System / shell variables | Machine or session | Yes | None |
direnv (.envrc file) | Per directory, automatic | Like .env | Manual |
| Secret manager (Vault, 1Password, SOPS) | Team, multi-project | Yes, encrypted and audited | Centralised, traceable |
direnv: load and unload automatically
Re-exporting variables by hand when switching projects is a source of mistakes. The direnv tool solves this by loading a directory’s variables as soon as the terminal enters it, and unloading them on the way out. The .envrc file can even delegate to the .env already present:
# .envrc — chargé automatiquement à l'entrée du dossier
dotenv .env
export PATH="$PWD/bin:$PATH"
# Autorisation explicite (sécurité de direnv) :
# direnv allowThis approach pairs well with a workstation already equipped with a well-chosen set of command-line tools: secrets follow the context, with no stray variable carried from one project to the next.
A secret that never entered the Git history never has to be revoked in a panic.
Rotation, sharing and production
Three habits set a mature project apart. First, rotation: a compromised — or simply old — key gets replaced, and the application must tolerate that change without a heavy redeploy. Second, sharing: beyond two or three people, swapping .env files over chat becomes unmanageable and dangerous — a secret manager centralises access and records who read what. Third, production: values there are injected by the hosting platform or the orchestrator, never by a committed file, and least privilege applies key by key.
A leak deserves a procedure written in advance: revoke the exposed key at the provider, generate a fresh one, purge the value from history if it entered it, and check the access logs. A key published to a public repository must be considered compromised within the minute, even if the repo is flipped back to private right away: indexing bots scan commits continuously.
Key takeaways
Managing secrets does not require an exotic tool to get started: a .gitignore written first, a versioned .env.example, and reading handled by the layer each framework provides cover the vast majority of projects. Sophistication — direnv, a secret manager, file encryption — is added as the team grows or secrets multiply. The one non-negotiable move is the first: nothing sensitive in the Git history.
On my own projects, the rule I enforce first is not technical but ritual: the .gitignore and the .env.example are written before a single line of business logic. I have seen too many Stripe keys and SMTP passwords leak because the .env was committed “just once” at the start of a project, when no one was paying attention. Systematic rotation after someone leaves the team has also spared me more than one bad surprise. — Simon Janvier
Further reading: the Config factor of the Twelve-Factor App methodology, the founding reference on separating code from configuration.
