Secrets & Config Validation
One of the most common production failures is a missing or misconfigured environment variable. The application boots without error, but a job fails hours later because STRIPE_SECRET is empty. Catching this at startup, not at runtime, is far less painful.
The Problem with Silent Defaults
Laravel's env() helper returns null if a variable is not set. This silently propagates through your code:
// config/services.php
'stripe' => [
'secret' => env('STRIPE_SECRET'), // null if not set
],
// PaymentService.php
$stripe = new Stripe(config('services.stripe.secret')); // Stripe SDK called with null
The SDK will likely throw an authentication error the first time a payment is attempted — not at startup.
Validating Config in a Service Provider
Validate required configuration values early by asserting they are present in a service provider:
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Config;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
$this->validateConfig();
}
private function validateConfig(): void
{
$required = [
'services.stripe.secret',
'services.mailgun.secret',
'queue.connections.redis.host',
];
foreach ($required as $key) {
if (empty(Config::get($key))) {
throw new \RuntimeException("Missing required configuration: [{$key}]");
}
}
}
}
This throws an exception during the boot phase, causing the application to fail immediately rather than silently.
Using a Dedicated Package
The spatie/laravel-missing-page-redirector and similar packages exist, but for config validation specifically, based/laravel-cloak and worksome/envy are popular choices. A simpler approach is a custom Artisan command:
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Config;
class ValidateConfig extends Command
{
protected $signature = 'config:validate';
protected $description = 'Validate that all required configuration values are set';
public function handle(): int
{
$required = [
'APP_KEY' => config('app.key'),
'DB_PASSWORD' => config('database.connections.mysql.password'),
'STRIPE_SECRET' => config('services.stripe.secret'),
'MAIL_USERNAME' => config('mail.mailers.smtp.username'),
];
$missing = array_filter($required, fn ($v) => empty($v));
if (!empty($missing)) {
$this->error('Missing required configuration:');
foreach (array_keys($missing) as $key) {
$this->line(" - {$key}");
}
return self::FAILURE;
}
$this->info('All required configuration is present.');
return self::SUCCESS;
}
}
Run this in your CI pipeline before deploying:
- name: Validate config
run: php artisan config:validate
Never Call env() Outside Config Files
Calling env() directly in application code bypasses the config cache. When you run php artisan config:cache, all env values are compiled into a cache file and env() returns null for everything. Always access environment variables through config values:
// ✅ Correct
$secret = config('services.stripe.secret');
// ❌ Wrong — returns null after config:cache
$secret = env('STRIPE_SECRET');
Sensitive Secrets in Production
- Never commit
.envfiles to version control. Use.env.exampleas the template. - Use your hosting platform's secret management for production values (Forge environment variables, AWS Secrets Manager, GitHub Actions secrets, Cloudflare Pages environment variables).
- Rotate secrets when team members leave or when a breach is suspected.
- Use different secrets per environment — your production Stripe key should never appear in a local
.env.
Tips
- Add a
config:validatecommand to your deployment pipeline so misconfigured environments are caught before traffic hits the new release. - Document every required environment variable in
.env.examplewith a comment explaining what it does. - Use
nullcoalescing with sensible defaults only for truly optional config, not for credentials.