Environment Sync
The env command manages environment variables across flavors in the shared/app_config package. It integrates with envied for type-safe, obfuscated, compile-time env var injection.
Architecture
shared/app_config uses a build-time flavor swap model:
shared/app_config/
.env.development # Source values for dev (gitignored — store in secret manager)
.env.staging # Source values for staging
.env.production # Source values for production
.env # ACTIVE — gitignored, written by env switch
lib/src/env/
env.dart # Single @Envied(path: '.env') class
env.g.dart # Generated (obfuscated XOR-encoded values)
app_env.dart # Thin public wrapper delegating to EnvA single Env class reads from .env. Before each build, env switch <flavor> copies the correct .env.<flavor> file into .env. Only the active flavor's secrets are compiled into the binary — production builds never contain development or staging secrets.
This replaces the previous approach of three separate envied classes (EnvDevelopment, EnvStaging, EnvProduction), each reading from its own file and all compiled into every binary.
Subcommands
env init
Initializes the environment configuration structure in shared/app_config:
dart run monorepo_toolkit env init
# Specify initial variables (defaults to API_BASE_URL and AES_KEY)
dart run monorepo_toolkit env init --var API_BASE_URL --var AES_KEY --var SECRETThis creates:
- Per-flavor
.env.<flavor>files (.env.development,.env.staging,.env.production) - A single
env.dartwith@Envied(path: '.env')reading from.env - Updates to the
FlavorStatusenum
env add
Adds a new environment variable to all flavor .env files and the env.dart class:
dart run monorepo_toolkit env add API_SECRETVariable names must start with an uppercase letter and contain only uppercase letters, numbers, and underscores (e.g., API_KEY, DATABASE_URL, SECRET_123).
This:
- Appends the variable to each
.env.<flavor>file - Adds the corresponding
@EnviedFieldannotation toenv.dart
You then fill in the actual values per flavor:
# .env.development
API_SECRET=dev_secret_key
# .env.staging
API_SECRET=staging_secret_key
# .env.production
API_SECRET=production_secret_keyAfter filling in values, run env switch <flavor> and then code generation.
env sync
Synchronizes the FlavorStatus enum with the flavors defined in flavors.yaml:
dart run monorepo_toolkit env syncRun this after adding or removing a flavor in flavors.yaml.
env switch
Copies .env.<flavor> to .env so that build_runner picks up the correct secrets for the requested flavor.
Synopsis:
dart run monorepo_toolkit env switch <flavor>Options:
| Option | Description |
|---|---|
--app-config-path | Path to shared/app_config (default: shared/app_config) |
Examples:
dart run monorepo_toolkit env switch development
dart run monorepo_toolkit env switch staging
dart run monorepo_toolkit env switch production
# Custom app_config location
dart run monorepo_toolkit env switch production --app-config-path path/to/app_configOutput:
✓ Switched env to: development
shared/app_config/.env.development -> shared/app_config/.env
Next: dart run build_runner build (regenerate env.g.dart)Error cases:
- Missing flavor argument →
Usage: env switch <flavor>(exit 64) .env.<flavor>file not found → error + list of available flavors (exit 1)
Full Build Workflow
# 1. Select the active flavor
dart run monorepo_toolkit env switch development
# 2. Regenerate obfuscated Dart code from .env
dart run build_runner build --delete-conflicting-outputs
# 3. Build
flutter build apk --flavor developmentAdding Environment Variables
- Add
@EnviedField()toenv.dart(or runenv add MY_VARto do it automatically) - Add the placeholder to all
.env.<flavor>files:MY_VAR=placeholder - Fill in actual values per flavor
- Run
dart run monorepo_toolkit env switch <current_flavor> - Run
dart run build_runner build --delete-conflicting-outputs
Integration with envied
env.dart uses @Envied(path: '.env', obfuscate: true). After env switch + build_runner, the generated env.g.dart contains XOR-encoded values — the plaintext secrets are never present in the compiled binary.
@Envied(path: '.env', obfuscate: true, useConstantCase: true)
abstract final class Env {
@EnviedField()
static final String apiBaseUrl = _Env.apiBaseUrl;
@EnviedField()
static final String aesKey = _Env.aesKey;
}Access values through AppEnv in application code:
final baseUrl = AppEnv.apiBaseUrl;CI Integration
Add these steps before the Flutter build step in your CI workflow:
GitHub Actions:
- name: Switch env flavor
run: dart run monorepo_toolkit env switch ${{ env.FLAVOR }}
- name: Generate env code
run: dart run build_runner build --delete-conflicting-outputs
working-directory: shared/app_config
- name: Build APK
run: flutter build apk --flavor ${{ env.FLAVOR }}
working-directory: apps/your_appFastlane:
before_all do |lane, options|
flavor = options[:flavor] || 'development'
sh("dart run monorepo_toolkit env switch #{flavor}")
sh("dart run build_runner build --delete-conflicting-outputs",
chdir: "shared/app_config")
endSecurity Model
.envis always gitignored — it contains the active flavor's plaintext secrets..env.<flavor>files should be gitignored and stored in a secret manager (e.g., GitHub Secrets, 1Password) for production/staging.env.g.dartcontains obfuscated values — committing it is acceptable; teams can also regenerate it in CI.- Production binaries compiled with
env switch productionwill never contain development or staging secrets.
Melos shortcuts
melos run env:init # Initialize env config
melos run env:add # Add env variable (pass var name after --)
melos run env:sync # Sync flavor enum
melos run env:switch -- <flavor> # Activate flavor (.env := .env.<flavor>)
melos run env:generate # Generate envied code