Environment Variables and Secrets Management
Development and Production servers are the standard deployment environments for running, testing and deploying high-availability applications. Each environment is configured with respective environment variables, allowing for structured release management, phased deployment and rollbacks in case of problems.
However, all environment variables are not the same, and can be classified in three buckets. Such a setup allows for a cleaner separation of concerns between environments, without accidentally leaking secrets. Let’s walkthrough the different buckets in Dart (Flutter).
Shared variables
Values don’t change across environments. They are defined in a shared module for easy access and updates. For example, API_ENDPOINT_LOGIN = ‘/auth/login’.
// package: constants/constants.dart
library constants;
/// Login API Endpoint
const String apiEndpointAuthLogin = ‘/api/login';
...
// file: api_client.dart
import 'package:constants/constants.dart' as constants;
...
final loginRequest = Uri.https(
<API_HOST>,
constants.apiEndpointAuthLogin,
);
...
Per-environment variables
Values differ between environments. They are public variables and can be checked in the repo. For example, API_HOST = ’localhost’ for development environment and API_HOST = ‘CUSTOM API SERVER’ for production environment.
// package: environments/environment.dart
library environments;
class Environment {
factory Environment() {
return _singleton;
}
Environment._internal();
static final Environment _singleton = Environment._internal();
/// DEV environment string.
static const String dev = 'DEV';
/// PROD environment string.
static const String prod = 'PROD';
/// Environment config.
late BaseConfig config;
/// Init environment config.
void initConfig(String environment) {
config = _getConfig(environment);
}
BaseConfig _getConfig(String environment) {
switch (environment) {
case Environment.prod:
return ProdConfig();
default:
return DevConfig();
}
}
}
abstract class BaseConfig {
String get apiHost;
}
/// Environment config for DEV.
class DevConfig implements BaseConfig {
@override
String get apiHost => 'localhost';
}
/// Environment config for PROD.
class ProdConfig implements BaseConfig {
@override
String get apiHost => ‘CUSTOM API SERVER’;
}
// file: main.dart
import 'package:environments/environment.dart';
...
Future<void> main() {
// Init environment config.
const environment = String.fromEnvironment(
'ENVIRONMENT',
defaultValue: Environment.dev,
);
Environment().initConfig(environment);
...
}
// file: api_client.dart
import 'package:constants/constants.dart' as constants;
import 'package:environments/environment.dart';
...
final loginRequest = Uri.https(
Environment().config.apiHost,
constants.apiEndpointAuthLogin,
);
...
// Runtime
$ flutter run --dart-define=ENVIRONMENT=DEV
$ flutter run --dart-define=ENVIRONMENT=PROD
Per-environment secrets
Values differ between environments. However, these variables should NOT be checked in the repo. For example, API_KEY = ‘DEV_API_KEY’ for development environment and API_KEY = ‘PROD_API_KEY’ for production environment.
// file: main.dart
import 'package:flutter_dotenv/flutter_dotenv.dart';
...
Future<void> main() {
...
// Load environment secrets.
dotenv.load(
fileName: 'assets/.$environment.env',
);
...
}
// file: api_client.dart
import 'package:constants/constants.dart' as constants;
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:http/http.dart' as http;
...
final loginResponse = await http.Client.get(
loginRequest,
headers: {
constants.headerAPIKey: dotenv.get('API_KEY'),
},
);
...
// file: assets/.DEV.env
API_KEY=DEV_API_KEY
// file: assets/.PROD.env
API_KEY=PROD_API_KEY
// file: .gitignore
...
*.env
...