Infrastructure SDKs
feature_flag_sdk Free
- Tier
- Freeversion 2.1.0
- Command
archipelago generate feature_flag_sdk- Generates
infrastructure/feature_flag_apiinfrastructure/feature_flag_implinfrastructure/flagr_feature_flag_implinfrastructure/remote_config_feature_flag_impl
- Depends on
- shared_app_config, shared_consent, network_sdkgenerate these first
- Used by
- app_debugger_feature_flag
Feature flag abstraction with pluggable vendor providers, in-memory caching, and multi-provider aggregation.
Generates a vendor-neutral FeatureFlagProvider contract plus a manager that adds caching, error handling, and multi-provider aggregation on top of it. A single featureFlagVendor variable selects which vendor package (if any) gets generated alongside the always-generated core.
Variables
| Variable | Type | Default | Description |
|---|---|---|---|
| appName | string | — | The name of your application |
| isForMonorepo | boolean | true | Whether this is being generated as part of a monorepo |
| featureFlagVendor | enum (flagr, remote_config, custom, none) | remote_config | Feature flag vendor implementation |
Only flagr and remote_config generate a vendor package. Choosing custom or none generates the vendor-neutral core only (feature_flag_api + feature_flag_impl) and prints a log message telling you to implement FeatureFlagProvider yourself and register it under FeatureFlagInjectorKey.
Usage
Interactive
archipelago generate feature_flag_sdkNon-interactive (CI)
archipelago generate feature_flag_sdk --config my_config.jsonGenerated Structure
Always generated:
infrastructure/
├── feature_flag_api/
│ ├── lib/
│ │ ├── feature_flag_api.dart
│ │ └── src/
│ │ ├── di/
│ │ │ └── feature_flag_injector_key.dart
│ │ └── domain/
│ │ ├── entities/
│ │ │ ├── evaluation_context.dart
│ │ │ ├── feature_flag_result.dart
│ │ │ └── flag_snapshot.dart
│ │ ├── enums/
│ │ │ └── aggregation_strategy.dart
│ │ └── repositories/
│ │ ├── feature_flag_delegate.dart
│ │ ├── feature_flag_provider.dart
│ │ └── feature_flag_snapshot_source.dart
│ ├── pubspec.yaml
│ └── README.md
└── feature_flag_impl/
├── lib/
│ ├── feature_flag_impl.dart
│ └── src/
│ ├── cache/
│ │ └── feature_flag_cache.dart
│ ├── di/
│ │ └── injector.dart # FeatureFlagImplPackageModule (di_modules manifest)
│ ├── feature_flag_aggregator.dart
│ ├── feature_flag_initializer.dart
│ └── feature_flag_manager.dart
├── pubspec.yaml
└── README.mdWhen featureFlagVendor is remote_config (the default):
infrastructure/
└── remote_config_feature_flag_impl/
├── lib/
│ ├── remote_config_feature_flag_impl.dart
│ └── src/
│ ├── di/
│ │ ├── injector.dart
│ │ └── register_module.dart
│ └── firebase_remote_config_feature_flag_provider.dart
├── pubspec.yaml
└── README.mdWhen featureFlagVendor is flagr:
infrastructure/
└── flagr_feature_flag_impl/
├── lib/
│ ├── flagr_feature_flag_impl.dart
│ └── src/
│ ├── di/
│ │ ├── injector.dart
│ │ └── register_module.dart
│ └── flagr_feature_flag_provider.dart
├── pubspec.yaml
└── README.mdpost_gen.dart deletes whichever of remote_config_feature_flag_impl / flagr_feature_flag_impl was not selected — the two are mutually exclusive in the generated output regardless of which value was chosen.
Public API
FeatureFlagProvider (feature_flag_api)
abstract class FeatureFlagProvider {
String get providerId;
Future<void> initialize();
Future<FeatureFlagResult> evaluate({
required String flagKey,
EvaluationContext? context,
bool defaultValue = false,
});
Future<Map<String, FeatureFlagResult>> evaluateAll({EvaluationContext? context});
Stream<FeatureFlagResult>? onFlagChanged(String flagKey);
Future<void> dispose();
}Implement this to add a new vendor. FeatureFlagResult carries enabled, an optional variantKey/variantValue for A/B tests, providerId, evaluatedAt, and metadata. isOn is true when enabled or variantKey is 'on'/'enable'.
FeatureFlagSnapshotSource (feature_flag_api)
An optional second interface for a provider. It lets tools such as the app_debugger_feature_flag spoofer list every flag the provider knows, and fetch on demand:
abstract interface class FeatureFlagSnapshotSource {
Future<Map<String, FlagSnapshot>> snapshot();
Future<void> fetchNow();
}
class FlagSnapshot {
const FlagSnapshot({
required String key,
required Object? value, // bool, num or String
required FlagValueType valueType,
required FlagSource source,
DateTime? fetchedAt,
});
factory FlagSnapshot.fromRaw({
required String key,
required String raw,
required FlagSource source,
DateTime? fetchedAt,
});
}
enum FlagValueType { boolean, string, number, json }
enum FlagSource { remote, cache, defaultValue }snapshot()returns every flag the provider knows, keyed by flag key, with its current value. Overrides from delegates are not applied.fetchNow()fetches and activates the latest values now, and ignores any minimum fetch interval.FlagValueType.jsonis a string value that parses as a JSON object or array. The value stays the raw string.FlagSource.cacheis a remote value from an earlier fetch, used when the last fetch failed or was throttled.FlagSource.defaultValueis an in-app or static default.FlagSnapshot.fromRawinfers the type of a raw string value:trueandfalsebecome abool, a JSON number (so not0123or0x1F) becomes anum, and a JSON object or array stays a string typedjson.
The interface is separate from FeatureFlagProvider, so a provider that does not implement it still compiles. The spoofer lists nothing for that provider. Both shipped vendors implement it.
FeatureFlagManager (feature_flag_impl)
The facade apps actually call:
final manager = getIt<FeatureFlagManager>();
final enabled = await manager.isEnabled(flagKey: 'new_checkout');
final result = await manager.evaluate(flagKey: 'new_checkout', defaultValue: false);
manager.invalidateCache('new_checkout');evaluate/isEnabled check an in-memory FeatureFlagCache (TTL-based, default 5 minutes / 100 entries) before delegating to a FeatureFlagAggregator, which combines every registered FeatureFlagProvider using an AggregationStrategy: firstMatch, priorityWithFallback (default), unanimous, or anyEnabled.
FeatureFlagInitializer (pre-launch registration + post-launch start)
FeatureFlagInitializer.register(getIt); // pre-launch
await FeatureFlagInitializer.initialize(getIt); // post-launchfeature_flag_impl depends only on feature_flag_api. It declares a hand-written FeatureFlagImplPackageModule under archipelago: di_modules in its own pubspec.yaml, wired into the app injector like any other infra package. That module's init() calls FeatureFlagInitializer.register(getIt), which registers FeatureFlagCache and FeatureFlagManager as lazy singletons in the base GetIt scope during pre-launch — never dropped when a feature scope is pushed/popped. The manager is built on first resolution from every provider key in FeatureFlagProviderRegistry (populated by each vendor's RegisterModule constructor). It reads FeatureFlagDelegateRegistry on every evaluation, so late delegates still apply.
FeatureFlagInitializer.initialize(getIt) runs in the post-launch phase and only calls manager.initialize() to start the providers. In template_base, post_launch_initializer.dart wraps this call in _FeatureFlagPostLaunchInitializer.
FeatureFlagInitializer.reset() is @visibleForTesting only — it clears the initializer's own _initialized flag but does not unregister anything from the GetIt instance.
FeatureFlagInjectorKey (feature_flag_api)
Named-instance keys used to register/discover providers: firebaseRemoteConfig, flagr, litmus, launchDarkly, customBackend, unleash, split, optimizely. Only firebaseRemoteConfig and flagr have a matching generated vendor package; the rest are reserved keys for a provider you register yourself. Discovery no longer probes this list directly — a vendor's RegisterModule constructor calls FeatureFlagProviderRegistry.instance.register(<key>), and FeatureFlagInitializer.register reads FeatureFlagProviderRegistry.instance.keys to build the provider list.
Delegate chain (FeatureFlagDelegate)
FeatureFlagManager.addDelegate (via the delegates constructor parameter) consults an override hook — used by things like the app_debugger_feature_flag spoofer — before falling through to cache and the real provider chain. Other packages queue a delegate through FeatureFlagDelegateRegistry.instance.register(x); FeatureFlagInitializer.register seeds the manager with FeatureFlagDelegateRegistry.instance.delegates in registration order. No changes to feature_flag_impl are needed to add a delegate.
Vendor notes
remote_config (Firebase Remote Config)
FirebaseRemoteConfigFeatureFlagProvider wraps firebase_remote_config (^6.5.6). initialize() calls setConfigSettings + fetchAndActivate(). Values are read via getValue/getAll; a value is considered "not fetched" when its source is ValueSource.valueStatic. Requires the same Firebase project setup (google-services.json / GoogleService-Info.plist) as firebase_analytics_impl. Firebase must be configured with flutterfire configure: see Firebase Setup. The brick wires a guarded Firebase.initializeApp() into bootstrap.dart (before initializeInjector()). Without Firebase config the app still boots and this vendor is skipped with a debug message. Also exposes forceRefresh(), which sets minimumFetchInterval to zero for one fetch and restores it afterward.
snapshot() lists getAll(). A value with source valueRemote is FlagSource.remote after a successful fetch in this session, and FlagSource.cache otherwise. valueDefault and valueStatic values are FlagSource.defaultValue. fetchedAt is lastFetchTime for remote values. fetchNow() fetches the same way as forceRefresh() but throws when the fetch fails; forceRefresh() returns false instead. Both restore minimumFetchInterval even when the fetch fails.
Fetch interval and cost
minimumFetchInterval defaults to 12 hours in release builds (releaseMinimumFetchInterval) and 1 minute in debug builds (kDebugMode). 12 hours is Firebase's recommended value. Since 2026-09-01, Remote Config bills fetches above 100,000 a day per Firebase project, so a short release interval multiplies cost with every daily session. To test a release or QA build against fresh values, call forceRefresh(), or pass minimumFetchInterval to the provider constructor in register_module.dart.
For flags that must change faster than 12 hours (kill switches, incident toggles), use Realtime Remote Config instead of a shorter interval. The provider's onFlagChanged() listens to onConfigUpdated, which the server pushes when a template is published, and activates the new values without a scheduled fetch.
flagr
FlagrFeatureFlagProvider calls POST {baseUrl}/api/v1/evaluation through the vendor-neutral HttpClient from network_sdk_api — it does not bundle its own HTTP dependency. FlagrFeatureFlagRegisterModule provides the base URL from AppEnv.flagrBaseUrl (FLAGR_BASE_URL in .env.<flavor>) and requires an HttpClient to already be registered in GetIt.
Adding the brick with featureFlagVendor: flagr to a project generated without Flagr also declares flagrBaseUrl in shared/app_config (Env, AppEnv) and FLAGR_BASE_URL in its .env* files. Running it again with another vendor removes the placeholder (a value you set is kept and reported), deletes the unselected vendor's package and removes its app dependency and injector module.
This means network_sdk must already be installed before generating feature_flag_sdk with featureFlagVendor: flagr. Unlike other hard cross-brick dependencies in this project (e.g. storage_sdk, self-generated), feature_flag_sdk's hooks do not self-generate network_sdk and do not warn if it is missing — choosing flagr without it fails to compile (package:network_sdk_api won't exist in the workspace). Install network_sdk first.
evaluateAll() always returns {} (Flagr has no bulk-evaluation endpoint) and onFlagChanged() always returns null (no real-time push support).
snapshot() calls GET {baseUrl}/api/v1/flags and lists every flag with its enabled state as a boolean, source remote. Variants need an entity to evaluate, so the snapshot does not show them. fetchNow() does nothing, because Flagr evaluates on every call and keeps no client cache.
Writing a custom provider
Choose featureFlagVendor: custom (or none), then add a package with your provider. Implement FeatureFlagProvider, and also implement FeatureFlagSnapshotSource if you want the debugger spoofer to list your flags:
class MyBackendFeatureFlagProvider(final MyFlagClient _client)
implements FeatureFlagProvider, FeatureFlagSnapshotSource {
@override
String get providerId => FeatureFlagInjectorKey.customBackend;
// initialize, evaluate, evaluateAll, onFlagChanged, dispose ...
@override
Future<Map<String, FlagSnapshot>> snapshot() async {
final values = await _client.cachedValues(); // Map<String, String>
return {
for (final MapEntry(:key, :value) in values.entries)
key: FlagSnapshot.fromRaw(
key: key,
raw: value,
source: _client.lastFetchSucceeded
? FlagSource.remote
: FlagSource.cache,
fetchedAt: _client.lastFetchTime,
),
};
}
@override
Future<void> fetchNow() => _client.fetchAndActivate(ignoreInterval: true);
}Register it the same way the shipped vendors do. Your RegisterModule constructor calls FeatureFlagProviderRegistry.instance.register(FeatureFlagInjectorKey.customBackend). It provides the provider as a FeatureFlagProvider named with the same key (@Named(FeatureFlagInjectorKey.customBackend)). FeatureFlagManager and the spoofer both find providers through that registry.
Keep snapshot() cheap and free of side effects: return the values the provider already holds, and fetch only in fetchNow(). Use FlagSnapshot.fromRaw when your vendor delivers strings. Build FlagSnapshot directly when the values are already typed. A JSON object or array must stay a string with FlagValueType.json.
After fetchNow(), FeatureFlagManager can still return a cached result for up to its cache TTL (5 minutes by default). Call FeatureFlagManager.clearCache() when app code must see the new values at once.
Dependencies
consent_api(shared/consent_api) —EvaluationContext.toMap()gates includingemailbehindConsentPurpose.targetingviaConsentGateRegistrynetwork_sdk_api+ a registeredHttpClient— required only whenfeatureFlagVendor: flagr(see above)firebase_remote_config: ^6.5.6— required only whenfeatureFlagVendor: remote_config
Changelog
Run archipelago brick info feature_flag_sdk to see the latest published version. Enterprise customers can read the full CHANGELOG.md in the brick source repository.