3 min read
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.