17 Reasons Your .env Is Doing Too Much
Almost every Laravel project I open has a long
.env, and only a handful of those lines are actually secret. The rest are decisions — an AI model name, a service URL, a timeout, a feature mode — sitting in a gitignored.envwhere nobody can review them, blame them, or see what the other environments use. This is a short argument for splitting those two things up, and a checklist for deciding whether it applies to you.
- The 17, interactive: /why
- The solution, running: laravel-env-settings.hpweb.dev
- Working with an AI agent? The package ships an AI assistant skill that teaches Claude Code, Cursor and Codex its conventions.
The .env file was designed for one job
Twelve-Factor's rule is that config belongs in the environment, and for secrets that rule is exactly right. A key in the environment is a key that is not in your repository.
But .env was the only mechanism Laravel handed us, so everything went in it. Not because anyone decided an AI model name should be hidden from the team — because there was nowhere else to put it. That is a convention we inherited, not a design.
The cost only shows up later: none of those non-secret lines has an author, a date, or a reason.
One question sorts every line
Does this value grant access to anything on its own?
STRIPE_SECRET=sk_live_… — yes. It stays in .env.
AI_TEXT_MODEL=gpt-4o — no. It is an infrastructure decision, and decisions belong where decisions are recorded: in code, in a pull request, with someone's name on them.
A hostname is a good test of whether you believe this. DB_HOST=prod-db.internal grants nobody anything — the database is behind a VPN either way. Hiding it buys no security and costs your team the ability to see which host each environment talks to.
The 17 symptoms
If you've hit these, the split above is what fixes them.
What belongs in .env
- Secrets and non-secrets share one hiding place, so every line is equally invisible to review — including the many that never needed hiding.
- The address is not the credential. Treating a host like a password hides information your team needs, for no security gain.
- Config changes leave no trace.
git log -p .envreturns nothing. Who raised the token limit, when, and why? Nobody can answer without asking around. - The same value, written out again and again. A currency or a rounding mode that is identical everywhere still gets copied into every
.envon every machine — and nothing enforces that the copies agree, so one of them eventually drifts. - One
.envfile can only describe one environment. Your.envdescribes the machine it is on. What staging uses is simply not in it.
Types instead of strings
- A typo returns
nullinstead of failing.config('services.auth.domian')is silently empty, and the bug surfaces three layers away as "host cannot be empty". - A value with a fixed set of options accepts any string.
'sandbx'commits, deploys and runs — then fails the first time a real payment is taken. - Settings scatter across unrelated lookups. Three
config()calls with no shared shape, and nothing to serialise when you want the lot as JSON.
Correct at runtime
env()returnsnullin production. It works locally and goes empty afterconfig:cache, which is the single most expensive lesson in this list.- A half-finished class deploys silently.
domain: ''with a// TODObeside it pushes green and gets discovered three days later. - Your
APP_ENVisqa,uatordemo, and the mapping that explains it lives in a different file from the values it selects.
Working with other people
- Checking production means logging into it. SSH and
grep— needing access, needing the box up, and putting secrets on your screen to answer a question about an AI model name. - Console output carries tokens into tickets. Paste a table into a chat thread and the query string goes with it, permanently.
- A test that changes one value must restate them all. Vary a single timeout and you are handed an
ArgumentCountErrorunless you rewrite every other property alongside it — so tests either grow boilerplate or quietly stop covering configuration. - Local tweaks fight with committed code. You bump a timeout to debug something,
git commit -am 'fix bug', and ship your laptop's setting to everyone.
Where it fits
- AI and LLM settings change constantly. Providers, models and token limits differ per environment and get changed most sprints — exactly the values that deserve a reviewer.
- Laravel's own keys are not part of this. See below; this is the boundary, not an oversight.
What the split looks like
# .env — only what must stay hidden
STRIPE_SECRET=sk_live_51H8xR2...
// app/Settings/AiSettings.php — reviewed in every PR
public static function development(): static
{
return new static(provider: 'ollama', text_model: 'llama3.2', max_tokens: 2000);
}
public static function production(): static
{
return new static(provider: 'openai', text_model: 'gpt-4o', max_tokens: 8000);
}
A short .env of real secrets to protect, instead of a long one where they hide among everything else. And git blame now answers the question .env never could:
a1b2c3d (Sara 3 weeks ago) text_model: 'gpt-4o',
9f8e7d6 (Marco 5 days ago) max_tokens: 8000,
The line that does not move
This is worth being blunt about, because it is where the idea stops.
Laravel's own configuration — APP_KEY, DB_*, MAIL_*, SESSION_* — and every third-party package's keys are read by config/*.php during bootstrap, before the container exists. You cannot resolve a settings class from a config file; the class isn't available yet.
Those keys stay in .env. This applies only to the configuration your application adds on top — which, in most projects, is the large majority of the .env file.
Which ones land for you?
I built the seventeen above into an interactive page, each one a before beside an after, so you can tick the ones you actually recognise rather than take my word for it:
🧭 Is this package for you? — the 17, side by side
If hardly any of them land, you probably don't need to change anything — that is a perfectly good outcome, and the page says so. If most of them do, laravel-env-settings is the package I developed to deal with them.