Keep secrets in .env, never in code
An API key hardcoded in a controller "just to test it quickly" has a way of outliving the test — and the commit. Every credential belongs in .env, referenced through config(), with the file itself never touching git. Here's the whole chain, including the two ways it quietly breaks anyway.
-
.env holds the value, .env.example holds the shape
.envis where the real key lives, and it ships with Laravel already listed in.gitignore— worth confirming, not assuming, especially in an older project..env.exampleis the committed template: every key name a fresh clone needs, with blank or dummy values, so setup is "copy this file" instead of "ask someone on Slack."# .env — real, gitignored STRIPE_SECRET=sk_live_51H8x... # .env.example — committed, no real value STRIPE_SECRET= -
Read it once, in a config file
env()belongs in exactly one place: a file underconfig/. Everywhere else in the app — controllers, jobs, service classes — pull the value throughconfig()instead. It's a small indirection that pays for itself in the next step.// config/services.php return [ 'stripe' => [ 'secret' => env('STRIPE_SECRET'), ], ]; // anywhere else — a controller, a job, a service class $secret = config('services.stripe.secret'); -
Why not just call env() everywhere
php artisan config:cacheflattens everyconfig/*.phpfile into a single cached file and stops reading.enventirely — that's what makes production boot faster. Anenv()call sitting outside a config file never gets baked into that cache, so it silently returnsnullthe moment caching is on, even though the exact same code worked fine locally. -
Keep it out of tests and seeders too
A real key hardcoded in a test or a seeder is just as committed as one in a controller. Point tests at
.env.testing(Laravel loads it automatically when the environment istesting) or fake the service entirely instead of hitting a real API with a real credential.// tests/Feature/CheckoutTest.php Http::fake([ 'api.stripe.com/*' => Http::response(['status' => 'succeeded'], 200), ]); -
Production secrets live in the host, not in a file
Deploying doesn't mean shipping
.envto the server. The hosting platform — Forge, Vapor, a container orchestrator's secret store, whatever's in use — sets the real environment variables directly, so the production credentials never exist as a file that could be copied, logged, or accidentally committed. -
If a secret already got committed
Deleting the line in a new commit isn't enough — the value is still sitting in every earlier commit's history, readable by anyone who clones the repo. Two things, in this order:
- Rotate the credential at the provider immediately — the leaked key stops being useful the moment it's replaced, regardless of what happens to the git history.
- Then, if the repo is shared or public, strip it from history with
git filter-repo(or the BFG Repo-Cleaner) and force-push the rewritten history.