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.
- The
.env
File: This file, located in your project root, stores all your environment-specific variables (database credentials, API keys, app URL, mail settings, etc.). Crucially,.env
should never be committed to version control (Git). It contains sensitive information and varies per environment. Your.gitignore
file should always include.env
. .env.example
: You do commit an.env.example
file. This serves as a template, showing all the environment variables your application expects. When a new developer joins or you set up a new environment, they copy.env.example
to.env
and fill in the appropriate values.- Accessing Configuration: Laravel's
config()
helper function reads values from your.env
file (via the cached configuration). For example,config('app.name')
orconfig('database.connections.mysql.host')
. - Configuration Caching: In production, always run
php artisan config:cache
. This compiles all your configuration files into a single cached file, significantly speeding up your application as it doesn't have to read multiple files on every request. Remember to re-run this command every time your configuration or.env
file changes in production. - Environment-Specific Files (Less Common for
.env
values, more for config files): While.env
handles most per-environment values, Laravel also allows for environment-specific configuration files. For example, if you haveconfig/app.php
, you could createconfig/staging/app.php
. Values in this staging-specific file would override the baseconfig/app.php
whenAPP_ENV
is set tostaging
. This is more for overriding actual PHP array config values rather than just.env
variables.
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:
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
).
- A different application ID (e.g.,
- 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
andProdConfig
)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
- How it works: You define different “flavors” (e.g.,
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.
- You can pass environment variables at build time using the
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.
- Similar to how web apps might load a JSON config file. You could bundle different JSON files (e.g.,
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
- Laravel:
.env
is king. Keep it out of Git. Use.env.example
. Cache in prod. - Flutter: Flavors are your best friend for managing distinct build configurations (dev, staging, prod) with their own API endpoints, keys, etc.
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