Skip to content
Back to archipelago.banuacoder.com

Infrastructure SDKs

feature_flag_sdk Free ​

Tier
Freeversion 2.1.0
Command
archipelago generate feature_flag_sdk
Generates
  • infrastructure/feature_flag_api
  • infrastructure/feature_flag_impl
  • infrastructure/flagr_feature_flag_impl
  • infrastructure/remote_config_feature_flag_impl
Depends on
shared_app_config, shared_consent, network_sdkgenerate these first

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 ​

VariableTypeDefaultDescription
appNamestring—The name of your application
isForMonorepobooleantrueWhether this is being generated as part of a monorepo
featureFlagVendorenum (flagr, remote_config, custom, none)remote_configFeature 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 ​

bash
archipelago generate feature_flag_sdk

Non-interactive (CI) ​

bash
archipelago generate feature_flag_sdk --config my_config.json

Generated Structure ​

Always generated:

text
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.md

When featureFlagVendor is remote_config (the default):

text
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.md

When featureFlagVendor is flagr:

text
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.md

post_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) ​

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

dart
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.json is a string value that parses as a JSON object or array. The value stays the raw string.
  • FlagSource.cache is a remote value from an earlier fetch, used when the last fetch failed or was throttled. FlagSource.defaultValue is an in-app or static default.
  • FlagSnapshot.fromRaw infers the type of a raw string value: true and false become a bool, a JSON number (so not 0123 or 0x1F) becomes a num, and a JSON object or array stays a string typed json.

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:

dart
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) ​

dart
FeatureFlagInitializer.register(getIt);   // pre-launch
await FeatureFlagInitializer.initialize(getIt);  // post-launch

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

dart
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 including email behind ConsentPurpose.targeting via ConsentGateRegistry
  • network_sdk_api + a registered HttpClient — required only when featureFlagVendor: flagr (see above)
  • firebase_remote_config: ^6.5.6 — required only when featureFlagVendor: 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.