Skip to content

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 Env

A 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:

bash
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 SECRET

This creates:

  • Per-flavor .env.<flavor> files (.env.development, .env.staging, .env.production)
  • A single env.dart with @Envied(path: '.env') reading from .env
  • Updates to the FlavorStatus enum

env add

Adds a new environment variable to all flavor .env files and the env.dart class:

bash
dart run monorepo_toolkit env add API_SECRET

Variable names must start with an uppercase letter and contain only uppercase letters, numbers, and underscores (e.g., API_KEY, DATABASE_URL, SECRET_123).

This:

  1. Appends the variable to each .env.<flavor> file
  2. Adds the corresponding @EnviedField annotation to env.dart

You then fill in the actual values per flavor:

bash
# .env.development
API_SECRET=dev_secret_key

# .env.staging
API_SECRET=staging_secret_key

# .env.production
API_SECRET=production_secret_key

After filling in values, run env switch <flavor> and then code generation.

env sync

Synchronizes the FlavorStatus enum with the flavors defined in flavors.yaml:

bash
dart run monorepo_toolkit env sync

Run 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:

bash
dart run monorepo_toolkit env switch <flavor>

Options:

OptionDescription
--app-config-pathPath to shared/app_config (default: shared/app_config)

Examples:

bash
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_config

Output:

✓ 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

bash
# 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 development

Adding Environment Variables

  1. Add @EnviedField() to env.dart (or run env add MY_VAR to do it automatically)
  2. Add the placeholder to all .env.<flavor> files: MY_VAR=placeholder
  3. Fill in actual values per flavor
  4. Run dart run monorepo_toolkit env switch <current_flavor>
  5. 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.

dart
@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:

dart
final baseUrl = AppEnv.apiBaseUrl;

CI Integration

Add these steps before the Flutter build step in your CI workflow:

GitHub Actions:

yaml
- 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_app

Fastlane:

ruby
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")
end

Security Model

  • .env is 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.dart contains obfuscated values — committing it is acceptable; teams can also regenerate it in CI.
  • Production binaries compiled with env switch production will never contain development or staging secrets.

Melos shortcuts

bash
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

Built by Banua Coder