fl_env 0.1.1
fl_env: ^0.1.1 copied to clipboard
Secure environment variable management for Flutter. CLI-encrypted .env files, decrypted natively at runtime — no plaintext at rest in the compiled binary.
fl_env #
Secure, encrypted environment variables for Flutter. .env files are encrypted at build time by the CLI — no plaintext secrets in your binary or your repo.
How it works #
Your machine / CI
.env.development ─┐
.env.staging ─┤──▶ fl_env build ──▶ fl_env_registry.bin ← commit (ciphertext)
.env.production ─┘ │ FlEnvRegistry.bin
└──────────── fl_env_key.bin ← gitignore (key)
FlEnvKey.bin
Device at runtime
*Registry.bin + *Key.bin ──▶ AES-256-GCM decrypt ──▶ FlEnvService.get('KEY')
- Master key (
FL_ENV_MASTER_KEY) — lives only in CI secrets and developer machines. Never in the repo. - Derived AES key — written to the consumer's native app module as a gitignored binary resource (
fl_env_key.bin/FlEnvKey.bin). Embedded in the compiled binary; not accessible from the plugin's own source. - Registry — encrypted values committed to the repo. Safe to commit; useless without the key.
Platform support #
| Android | iOS | Web |
|---|---|---|
| ✅ | ✅ | ⚠️ stub |
Web returns empty values in Phase 1 (native crypto only). Android requires API 23+.
Installation #
# pubspec.yaml
dependencies:
fl_env: ^0.1.0
Quick start #
1 — Scaffold the config #
Run from your Flutter project root:
dart run fl_env setup
This creates fl_env.yaml, appends .gitignore entries, and installs the iOS Podfile hook that adds the binary resources to Xcode's Copy Bundle Resources phase automatically.
2 — Generate a master key #
export FL_ENV_MASTER_KEY=$(dart run fl_env keygen)
Store this value in your CI secrets (Settings → Secrets → Actions → FL_ENV_MASTER_KEY). Never commit it.
3 — Edit fl_env.yaml #
The scaffold creates a starting point — edit to match your project:
fl_env:
default_env: development
output:
android: android/app/src/main
ios: ios/Runner
tiers:
development: .env
staging: .env.staging
production: .env.production
required_keys:
- API_URL
- API_KEY
key_types:
TIMEOUT: int
DEBUG: bool
See the fl_env.yaml reference below for all options.
4 — Create your .env files #
# .env (gitignored — fill in real values)
API_URL=https://dev.api.example.com
API_KEY=dev_key_xxxxxxxxxxxx
TIMEOUT=30
DEBUG=true
TAGS=flutter,dart,dev
Commit a .env.example with safe dummy values so teammates can bootstrap:
# .env.example (committed — safe placeholder values)
API_URL=https://dev.api.example.com
API_KEY=dev_key_xxxxxxxxxxxx
TIMEOUT=30
DEBUG=true
TAGS=flutter,dart,dev
5 — Build the encrypted registry #
dart run fl_env build
This writes four files into your consumer app's own directories:
| File | Destination | Commit? |
|---|---|---|
fl_env_key.bin |
android/app/src/main/res/raw/ |
❌ no |
fl_env_registry.bin |
android/app/src/main/res/raw/ |
✅ yes |
FlEnvKey.bin |
ios/Runner/ |
❌ no |
FlEnvRegistry.bin |
ios/Runner/ |
✅ yes |
6 — iOS: run pod install #
cd ios && pod install
The Podfile hook installed by fl_env setup adds FlEnvKey.bin and FlEnvRegistry.bin to Xcode's Copy Bundle Resources phase automatically. This only needs to happen once (or after a fresh clone).
7 — Initialize in Dart #
// main.dart
import 'package:fl_env/fl_env.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await FlEnvService.instance.init();
runApp(const MyApp());
}
Then read values from anywhere in your app:
final url = FlEnvService.instance.get('API_URL'); // String?
final timeout = FlEnvService.instance.getInt('TIMEOUT') ?? 30; // int
final debug = FlEnvService.instance.getBool('DEBUG') ?? false; // bool
final tags = FlEnvService.instance.getList('TAGS'); // List<String>?
fl_env.yaml reference #
fl_env:
default_env: development # tier name to load at runtime
output:
# Paths relative to fl_env.yaml (your project root).
# fl_env build writes key + registry into these directories.
android: android/app/src/main
ios: ios/Runner
tiers:
# Map tier name → path to the .env file (relative to fl_env.yaml)
development: .env
staging: .env.staging
production: .env.production
# Optional: every listed key must be present in every tier's .env file.
# fl_env check exits 1 if any are missing.
required_keys:
- API_URL
- API_KEY
# Optional: declare the expected type for specific keys.
# fl_env check verifies the value is coercible before a build.
# Supported types: string (default), int, double, bool, uri, list
key_types:
TIMEOUT: int
DEBUG: bool
API_URL: uri
# Optional: keys whose values are redacted in fl_env inspect output.
# Defaults: SECRET, KEY, TOKEN, PASSWORD, PRIVATE, CREDENTIAL, AUTH
sensitive_key_patterns:
- SECRET
- KEY
- TOKEN
CLI commands #
All commands accept --project <path> to target a directory other than the current working directory.
| Command | What it does |
|---|---|
fl_env setup |
Writes fl_env.yaml, updates .gitignore, installs iOS Podfile hook |
fl_env keygen |
Prints a fresh 64-char hex master key to stdout |
fl_env build [--env=<name>] |
Encrypts all tiers, writes registry + key binary files |
fl_env inspect [--env=<name>] |
Prints Tier 1 key-value pairs from the .env file; redacts sensitive values |
fl_env scan |
Lists discovered .env files and their resolved tier names |
fl_env check |
Detects drift, validates required_keys and key_types; exits 1 on failure (CI gate) |
Dart API #
Typed accessors #
| Method | Return type | Throws if unparseable |
|---|---|---|
get(key) |
String? |
— |
getRequired(key) |
String |
FlEnvKeyNotFoundException |
getInt(key) |
int? |
FlEnvTypeCastException |
getRequiredInt(key) |
int |
FlEnvKeyNotFoundException / FlEnvTypeCastException |
getBool(key) |
bool? |
FlEnvTypeCastException |
getDouble(key) |
double? |
FlEnvTypeCastException |
getUri(key) |
Uri? |
FlEnvTypeCastException |
getList(key, {separator}) |
List<String>? |
— |
activeEnvironment |
String |
— |
All accessors throw FlEnvNotInitializedException if called before init().
getBool truth table (case-insensitive):
| Value | Result |
|---|---|
true / 1 / yes |
true |
false / 0 / no |
false |
| anything else | FlEnvTypeCastException |
Testing #
Use FlEnvFakeChannel to bypass the native layer in unit tests:
import 'package:fl_env/fl_env.dart';
import 'package:test/test.dart';
void main() {
setUp(() async {
await FlEnvService.instance.init(
channel: const FlEnvFakeChannel({
'API_URL': 'http://localhost:8080',
'DEBUG': 'true',
'TIMEOUT': '5',
}),
);
});
tearDown(() => FlEnvService.instance.reset());
test('reads API_URL', () {
expect(FlEnvService.instance.get('API_URL'), 'http://localhost:8080');
});
test('parses DEBUG as bool', () {
expect(FlEnvService.instance.getBool('DEBUG'), isTrue);
});
}
FlEnvFakeChannel also accepts an activeTier parameter (defaults to 'test').
CI integration #
Add these steps before the native build step:
- name: Generate env registry
env:
FL_ENV_MASTER_KEY: ${{ secrets.FL_ENV_MASTER_KEY }}
run: |
cp .env.example .env
cp .env.staging.example .env.staging
cp .env.production.example .env.production
dart run fl_env build
- name: Assert registry is up-to-date
run: dart run fl_env check
fl_env check exits 1 if any .env file has changed since the last fl_env build, or if any required_keys are missing, or if any key_types values are not coercible — a reliable gate that catches misconfiguration before a native build wastes several minutes.
Add FL_ENV_MASTER_KEY to your repository secrets: Settings → Secrets → Actions → New repository secret.
Exception reference #
| Code | Class | Thrown when |
|---|---|---|
FL_ENV_E001 |
FlEnvNotInitializedException |
Any accessor called before init() |
FL_ENV_E002 |
FlEnvInitException |
init() fails (native decryption error) |
FL_ENV_E003 |
FlEnvKeyNotFoundException |
getRequired* called for an absent key |
FL_ENV_E004 |
FlEnvTypeCastException |
Value cannot be parsed to the requested type |
FL_ENV_E005 |
FlEnvPhaseException |
switchEnvironment() called (Phase 2 only) |
All exceptions extend FlEnvException and expose message, suggestion, code, and documentationUrl (pointing to https://fl-env.pixmerc.com/errors/<code>).
Security model #
| Aspect | Detail |
|---|---|
| Encryption | AES-256-GCM per value, unique 12-byte nonce per entry |
| Key derivation | HKDF-SHA256 with info "fl_env v1" (domain separation) |
| Master key | CI secrets + developer machine only — never committed |
| Derived key (Phase 1) | Gitignored binary resource in consumer app module (fl_env_key.bin / FlEnvKey.bin), embedded in compiled binary |
| Derived key (Phase 2) | Android Keystore / iOS Secure Enclave migration (planned) |
| Registry | Ciphertext committed to repo — safe because decryption requires the key |
.gitignore requirements #
fl_env setup appends these automatically. If you prefer to add them manually:
# fl_env generated files — do not commit
android/app/src/main/res/raw/fl_env_key.bin
android/app/src/main/res/raw/fl_env_registry.bin
ios/Runner/FlEnvKey.bin
ios/Runner/FlEnvRegistry.bin
# Lockfile (tracks .env hashes for drift detection)
.fl_env.lock
# Source .env files — commit only *.example templates
.env
.env.*
!.env.example
!.env.*.example
License #
MIT — see LICENSE.