Environment Variables
Declare your env keys once, in a typed class. Then env.appPort is an int, your IDE suggests every key, and a missing key stops the app at startup.
The typed env class
lib/config/env.dart:
import 'package:dox_core/dox_core.dart';
part 'env.g.dart';
enum LogLevel { debug, info, error }
@DoxEnv()
abstract class AppEnv {
static AppEnv load([EnvReader? reader]) {
return _$AppEnv.load(reader);
}
String get appName; // APP_NAME, required
String get appKey; // APP_KEY, required
int get dbPort => 5432; // DB_PORT, default 5432
bool get appDebug => false; // APP_DEBUG, default false
LogLevel get logLevel => LogLevel.info; // LOG_LEVEL=debug
Duration get cacheTtl => const Duration(minutes: 5); // CACHE_TTL=5m
List<String> get corsOrigins => <String>[]; // CORS_ORIGINS=a.com,b.com
@EnvKey('AWS_BUCKET')
String? get s3Bucket; // optional, null when not set
}
/// The env of the app. Read and checked once, on first use.
final AppEnv env = AppEnv.load();
Run dart run build_runner build (or dox build_runner:build). It writes env.g.dart.
Use it anywhere:
import 'package:my_app/config/env.dart';
DatabaseConfig databaseConfig = DatabaseConfig(
connection: 'postgres',
connections: <String, ConnectionConfig>{
'postgres': ConnectionConfig(port: env.dbPort /* int */, ...),
},
);
The rules
- Key: the getter name in UPPER_SNAKE case.
appNamereadsAPP_NAME,s3BucketreadsS3_BUCKET. Use@EnvKey('X')for another name. - Default: the getter body. It can be any expression, like
Platform.numberOfProcessors. It runs only when the key is missing. A default can not read another env getter. - Optional: a nullable getter with no body (
String? get s3Bucket;). It isnullwhen the key is missing. - Required: any other getter with no body. A missing key stops the app.
- Empty: an empty value (or the text
null) counts as missing.
Types
| Type | Value in .env |
|---|---|
String |
any text |
int, double, num |
3000, 0.5 |
bool |
true/false, 1/0, yes/no, on/off (any case) |
Uri |
https://dox.dev |
Duration |
a number and a unit: 500ms, 30s, 5m, 2h, 1d. A plain number is seconds: 300 is 5 minutes. |
| an enum | the name of a value, any case: debug |
List<String> |
comma separated: a, b, c. Empty items are dropped. |
Any other type is a build error.
Fail fast
AppEnv.load() reads every key one time. Then it checks them all, and throws one EnvException that lists every missing or bad key:
The env variable is made on first use. The config files use it, so this happens when the app starts. After that, env.dbPort is a plain field read. There is no lookup and no parsing on each request.
Where values come from
The environment wins
A real environment variable (set in the shell, Docker or systemd) wins over the .env file.
So you can keep .env for development and use real variables in production.
The .env file is optional.
Commands
dox env:generate # make lib/config/env.dart from .env.example
dox env:check # load the env and print every missing or bad key
dox env:generate reads .env.example:
- digits become an
intwith that default:APP_PORT=3000givesint get appPort => 3000; true/falsebecome aboolwith that default- an empty value becomes an optional
String? - any other value becomes a required
String. The example value is never a default, so a secret in the example can not leak into the code. #comment lines above a key become its doc comment
It does not overwrite an existing lib/config/env.dart. Run dox build_runner:build after it.
dox env:check loads AppEnv from lib/config/env.dart, with the real environment and .env. It exits with code 1 when a key is missing or bad. Use it in CI or before a deploy.
Tests
Pass your own values to load(). Nothing is read from the shell or .env:
AppEnv env = AppEnv.load(
EnvReader(
environment: <String, String>{'APP_KEY': 'test'},
dotEnv: <String, String>{'DB_PORT': '5433'},
),
);
Env.get: dynamic keys
Env.get() still works. Use it for a key you only know at run time. For normal config, use the typed class.
Env.get('APP_KEY'); // String, '' when missing
Env.get('APP_KEY', 'default_value');
Env.get<int>('APP_PORT', 3000);
Env.get<bool>('SOCKETIO_ENABLED', false); // true, 1, yes, on
Env.get<String?>('AWS_BUCKET'); // null when it is not set
A missing or empty key gives the default. With no default:
Stringgives an empty string''. It is never the text"null".String?anddynamicgivenull.int,num,doubleandboolthrow anArgumentErrorwith the key name. A value that is not a number throws too.
Env.get() looks up and converts the value on every call. Do not call it on a hot path.
APP_KEY
When APP_KEY is empty and APP_ENV=production, Dox().startServer() stops with an error.
In other environments it logs a warning. Make a key with dox key:generate.