paulund
#laravel #configuration #security

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 .env files to version control. Use .env.example as 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:validate command to your deployment pipeline so misconfigured environments are caught before traffic hits the new release.
  • Document every required environment variable in .env.example with a comment explaining what it does.
  • Use null coalescing with sensible defaults only for truly optional config, not for credentials.