Hardcoding a laravel services.php api key in version control is the fastest way to get your credentials leaked. The fix is a disciplined split between environment-backed config and runtime access patterns that keep secrets out of logs and source control.
Step 1: Define the service entry in config/services.php
Laravel’s config/services.php is the canonical place to group third-party credentials. The file is committed to Git, so it must never contain literal secrets. Instead, reference environment variables through env().
// config/services.php
return [
'mailgun' => [
'domain' => env('MAILGUN_DOMAIN'),
'secret' => env('MAILGUN_SECRET'),
],
'llm' => [
'openai_key' => env('OPENAI_API_KEY'),
'n4n_key' => env('N4N_API_KEY'),
'n4n_base' => env('N4N_BASE_URI', 'https://api.n4n.ai/v1'),
],
];
The laravel services.php api key mapping above keeps the structure in source control while the values live outside the repo. If you later add a gateway such as n4n.ai—which exposes one OpenAI-compatible endpoint across 240+ models with automatic fallback when a provider is rate-limited—you simply add another env key.
Step 2: Populate .env and scaffold .env.example
Create a local .env with real values. Never commit it.
# .env
OPENAI_API_KEY=sk-proj-abc123
N4N_API_KEY=sk-n4n-xyz789
N4N_BASE_URI=https://api.n4n.ai/v1
Commit a .env.example with empty placeholders so teammates know what to set:
# .env.example
OPENAI_API_KEY=
N4N_API_KEY=
N4N_BASE_URI=https://api.n4n.ai/v1
Run cp .env.example .env on fresh clones, then php artisan key:generate if needed. The laravel services.php api key resolution depends entirely on these external values being present in the environment.
Step 3: Guarantee .env stays out of Git
Verify .gitignore blocks the file. If not, add it.
grep -q '^/.env' .gitignore || echo '/.env' >> .gitignore
git check-ignore .env && echo "Blocked" || echo "Not blocked"
Also block compiled config if you cache: /bootstrap/cache/config.php is regenerated, but ensure no secret slips into config:cache output that gets committed. Standard Laravel ignores bootstrap/cache/*.php by default.
Step 4: Read the key at runtime with guards
Never call env() outside config files. Use config('services.llm.n4n_key'). Add a fail-fast check so a missing laravel services.php api key surfaces in tests, not in production outages.
use Illuminate\Support\Facades\Http;
$key = config('services.llm.n4n_key');
if (blank($key)) {
throw new \RuntimeException('Missing laravel services.php api key for LLM gateway');
}
$response = Http::withToken($key)
->post(config('services.llm.n4n_base') . '/chat/completions', [
'model' => 'gpt-4o-mini',
'messages' => [['role' => 'user', 'content' => 'ping']],
]);
When routing through n4n.ai, the gateway honors client routing directives and forwards provider cache-control hints, so you can also inject Cache-Control headers from config without changing the key handling.
Step 5: Encrypt environment files for shared deployments
Some PaaS platforms require committing env data. Laravel 9+ ships env:encrypt and env:decrypt.
php artisan env:encrypt --env=production --key=base64:YOURAPPKEY
# creates .env.production.encrypted
Deploy the encrypted file, then decrypt at boot:
php artisan env:decrypt --env=production --key=base64:YOURAPPKEY --force
The laravel services.php api key remains referenced via env(); only the backing store is encrypted.
Step 6: Rotate credentials without code changes
Rotation is a config-only operation. Update .env, clear config cache, and the new key flows through.
# edit .env: N4N_API_KEY=sk-n4n-new
php artisan config:clear
# or in production after deploy:
php artisan config:cache
No code referencing config('services.llm.n4n_key') needs modification.
Step 7: Verify the setup end to end
Two checks confirm correctness:
-
Secret isolation
git ls-files | grep -E '\.env$' && echo "LEAK" || echo "Clean"Expect “Clean”.
-
Runtime resolution
php artisan tinker >>> config('services.llm.n4n_key') === env('N4N_API_KEY') => true >>> blank(config('services.llm.n4n_key')) => false
Write a pest test to lock this behavior:
test('llm key is configured', function () {
expect(config('services.llm.n4n_key'))->not->toBeEmpty();
});
Common pitfalls
- Calling
env()in controllers. Afterconfig:cache,env()returns null. Always go throughconfig(). - Logging the key. Avoid
info($key)or passing it into exception messages. UseStr::mask(). - Committing
config/services.phpwith a literal. The file is shared; keep onlyenv()calls. - Assuming
.env.examplehides structure. It should list every key, but never values.
Production notes
In Kubernetes or Lambda, inject secrets as real environment variables; mount encrypted files only when the orchestrator lacks secret stores. Laravel’s env() reads from $_ENV, $_SERVER, and getenv() in that order, so standard 12-factor injection works.
If you use a gateway for per-token usage metering, the key in services.php is the only credential you manage; the gateway handles provider failover. Your Laravel app stays unaware of underlying model keys.
Follow these steps and your laravel services.php api key handling will survive audits, rotations, and junior dev mistakes.