App Configuration: Keeping Dev, Staging, & Prod Sane (Laravel + Flutter)

Hey everyone, Jamie here.

We've delved into building features, deploying them, and even monitoring them. But there's a crucial, often less glamorous, aspect of development that can save you immense headaches (or cause them if mishandled): application configuration.

Your Laravel API needs different database credentials for development versus production. Your Flutter app needs to point to http://localhost:8000/api when you're coding locally, but https://api.yourdomain.com when it's live. API keys for third-party services (like Sentry, Pusher, or payment gateways) will definitely be different for your test environment versus your production environment.

Managing these variations effectively across development, staging, and production is vital for a smooth workflow and a stable application. Let's look at some pragmatic approaches for both our Laravel backend and Flutter frontend.

Laravel Configuration: The .env Powerhouse

Laravel makes backend configuration management relatively straightforward thanks to its reliance on .env files.

Key Laravel Configuration Practices: 1. NEVER commit .env. 2. Always keep .env.example up-to-date. 3. Use config('your.key', 'default_value') to provide defaults. 4. Cache your config in production: php artisan config:cache. 5. Securely manage your production .env files on your server (e.g., using your hosting platform's environment variable management, or tools like Laravel Forge/Envoyer).

Flutter Configuration: Flavors and .dart Files

Managing configuration in Flutter requires a different approach, as .env files aren't a native concept in the same way. The goal is to build different versions of your app (e.g., dev, staging, prod) that are hardcoded with the correct settings for that environment.

Here are common strategies:

  1. Build Flavors (Schemes on iOS): This is the most robust and recommended approach. Flavors allow you to define completely separate build configurations for your app.

    • How it works: You define different “flavors” (e.g., dev, staging, production). Each flavor can have:
      • A different application ID (e.g., com.example.myapp.dev, com.example.myapp.staging, com.example.myapp). This allows you to install all versions on the same device.
      • Different app names (e.g., “MyApp Dev”, “MyApp Staging”, “MyApp”).
      • Different entry points (main_dev.dart, main_staging.dart, main_prod.dart).
    • Configuration Files per Flavor: Inside each main_<flavor>.dart, you can set up your environment-specific configurations.

    Example Structure:

    lib/
      main_dev.dart
      main_staging.dart
      main_prod.dart
      config/
        app_config.dart
        dev_config.dart
        staging_config.dart
        prod_config.dart
      src/
        app.dart
        // ... rest of your app code
    

    lib/config/app_config.dart (Base class/interface):

    abstract class AppConfig {
      String get appName;
      String get apiBaseUrl;
      String get sentryDsn;
      // Add other config properties
    }
    

    lib/config/dev_config.dart:

    import 'app_config.dart';
    
    class DevConfig implements AppConfig {
      @override
      String get appName => "MyApp Dev";
      @override
      String get apiBaseUrl => "[http://10.0.2.2:8000/api](http://10.0.2.2:8000/api)"; // Android emulator localhost
      // String get apiBaseUrl => "http://localhost:8000/api"; // iOS simulator/web
      @override
      String get sentryDsn => "YOUR_DEV_SENTRY_DSN";
    }
    

    (Similarly for StagingConfig and ProdConfig)

    lib/main_dev.dart:

    import 'package:flutter/material.dart';
    import 'config/dev_config.dart';
    import 'src/app.dart'; // Your main App widget
    
    void main() {
      final config = DevConfig();
      // You can make `config` available globally or via dependency injection
      // For example, pass it to your App widget or use a service locator.
      runApp(MyApp(config: config));
    }
    

    You then build/run a specific flavor: flutter run --flavor dev -t lib/main_dev.dart flutter build apk --flavor prod -t lib/main_prod.dart

  2. Using --dart-define from the Command Line:

    • You can pass environment variables at build time using the --dart-define flag.
    • flutter run --dart-define=API_BASE_URL=http://localhost:8000/api --dart-define=SENTRY_DSN=your_dev_dsn
    • In your Dart code, you access these using: const String apiBaseUrl = String.fromEnvironment('API_BASE_URL', defaultValue: 'https://api.prod.com');
    • Pros: Simple for a few variables.
    • Cons: Can get unwieldy for many variables. Values are strings, so you might need parsing. Less type-safe than dedicated config classes. Secrets are visible in build commands/CI logs if not handled carefully.
  3. Separate Configuration Files Loaded at Runtime (Less Common for Mobile):

    • Similar to how web apps might load a JSON config file. You could bundle different JSON files (e.g., config_dev.json, config_prod.json) as assets and load the appropriate one at startup.
    • Pros: Configuration is external to the Dart code.
    • Cons: Adds an async step at app startup to load config. Managing asset bundling per flavor can be tricky. Not as “clean” as compile-time configuration via flavors.

Key Flutter Configuration Practices: 1. Use Flavors for distinct environments. It's the most comprehensive solution. 2. Define clear configuration classes/objects for type safety and easy access. 3. NEVER commit production API keys or sensitive secrets directly into your Dart code that gets committed to public/shared repositories. * For flavors, these secrets would live in your main_prod.dart or prod_config.dart which are committed. This is generally acceptable if your repository is private. * If using --dart-define in CI/CD, store the secrets in your CI/CD system's secret management and pass them via --dart-define. * For open-source apps, you might provide an example config and require users to create their own with their keys.

Keeping Them In Sync

While Laravel and Flutter have different mechanisms, the values for things like API endpoints need to align. There's no magic bullet here other than good old-fashioned discipline and clear documentation within your team. Ensure your Flutter prod_config.dart points to the same API URL that your Laravel production .env file's APP_URL implies.

The Pragmatic Summary

Setting up robust configuration management from the start of a project, even if it feels like a bit of extra work, will pay dividends in the long run by reducing errors, simplifying deployments, and making it easier to onboard new team members.

How do you handle configuration across your full stack? Any favourite tips or tools? Share them in the comments!

Cheers,

Jamie C