Feature SDKs (Enterprise)
app_debugger_feature_flag Enterprise
- Tier
- Enterpriseversion 2.0.2
- Command
archipelago generate app_debugger_feature_flag- Generates
features/app_debugger_feature_flag/app_debugger_feature_flagfeatures/app_debugger_feature_flag/app_debugger_feature_flag_apifeatures/app_debugger_feature_flag/app_debugger_feature_flag_noop
- Generates first
- app_debuggerwhen needed and missing, so there is nothing to install first
- Depends on
- shared_identifiers, feature_flag_sdkgenerate these first
Feature flag spoofer sub-brick for
app_debugger. Adds a Feature Flags tool to the debugger that lists every flag your providers know and lets developers override them at runtime, without touching the real provider chain. Automatically swapped to a no-op stub in production-flavor builds viabuild_prepare.yaml.


Variables
| Variable | Default | Description |
|---|---|---|
uiKitPackageName | app_ui_kit | The project's UI kit package. Read from archipelago.yaml in pre_gen. |
prefix | App | The UI kit class prefix (AppTextField). Read from archipelago.yaml in pre_gen. |
The CLI fills both from archipelago.yaml. Pass them only to raw mason make in CI.
Prerequisites
app_debugger, generated first when the project lacks it. It providesapp_debugger_api(theDebuggerPanelPageRegistrycontract) and thedi/debug_tools.dartsentinels this brick'spost_gen.dartpatches (moved off the host app'sbootstrap.dartin 1.1.1 — see below).- A
feature_flag_implpackage configured in your app for real flag evaluation. feature_flag_sdk2.1.0 or newer. The spoofer lists flags throughFeatureFlagSnapshotSourceinfeature_flag_api. On an olderfeature_flag_api,pre_genstops before it writes anything and asks you to upgradefeature_flag_sdkfirst.
Usage
Interactive
archipelago generate app_debugger_feature_flagNon-interactive (CI)
archipelago generate app_debugger_feature_flag --config my_config.jsonGenerated Structure
features/
└── app_debugger_feature_flag/
├── app_debugger_feature_flag_api/
│ └── lib/src/
│ ├── feature_flag_spoofer_sdk.dart # FeatureFlagSpooferSDK contract
│ ├── spoofer_ui_kit.dart # SpooferUiKit port
│ ├── material_spoofer_ui_kit.dart # Material fallback, extend it
│ └── models/flag_override.dart # FlagOverride model
├── app_debugger_feature_flag/
│ └── lib/src/
│ ├── feature_flag_spoofer_sdk_impl.dart
│ ├── feature_flag_spoofer_delegate.dart
│ ├── feature_flag_spoofer_page.dart # "Feature Flags" debugger tool
│ ├── kit_spoofer_ui_kit.dart # default SpooferUiKit on your UI kit
│ └── di/injector.dart
└── app_debugger_feature_flag_noop/
└── lib/src/di/injector.dart # production-flavor stub, hand-written (no codegen)API
abstract interface class FeatureFlagSpooferSDK implements FeatureSDK {
bool get isActive;
Stream<List<FlagOverride>> get overridesStream;
List<FlagOverride> get currentOverrides;
void setOverride(FlagOverride override);
void removeOverride(String flagKey);
void clearAllOverrides();
bool get isSpoofing; // false: overrides kept, not applied
void setSpoofing({required bool enabled});
Future<Map<String, FlagSnapshot>> snapshot();
Future<void> fetchNow();
FeatureFlagDelegate? get delegate;
}
class FlagOverride {
const FlagOverride({required this.flagKey, required this.value, this.enabled = true});
final String flagKey;
final FeatureFlagResult value;
final bool enabled; // false suspends the override without removing it
}Since 1.1.1, post_gen.dart wires the delegate for you — no manual step required. It injects this into app_debugger's own di/debug_tools.dart, inside a dedicated archipelago:feature_flag_spoofer_delegate sentinel that runs right after the eager debug_panel_pages resolution:
final spooferDelegate = GetIt.I<FeatureFlagSpooferSDK>().delegate;
if (spooferDelegate != null) {
FeatureFlagDelegateRegistry.instance.register(spooferDelegate);
}FeatureFlagDelegateRegistry lives in feature_flag_api — this brick's hook adds feature_flag_api (not feature_flag_impl) as a dependency of app_debugger, so a debug-only package never pulls in a release impl. FeatureFlagManager (from feature_flag_impl) reads the registry on every evaluation, so the delegate applies whenever it registers. The null guard is what makes this safe in release builds, where the noop stub's delegate getter returns null — so nothing is registered in production.
How it works
FeatureFlagSpooferSDKImpl(@Singleton(as: FeatureFlagSpooferSDK), eager since 1.2.1) constructs aFeatureFlagSpooferDelegatebacked by an in-memoryMap<String, FlagOverride>and registers aFeatureFlagSpooferPagewithDebuggerPanelPageRegistry— the debugger home lists it as a tool without ever importing this package.FeatureFlagDelegateis an override hook that runs before the real provider chain;FeatureFlagSpooferDelegate.evaluatereturnsoverride.valuefor an enabled override, ornullto defer to the next delegate/real provider.- The panel page lists every flag the providers know.
snapshot()reads each provider registered inFeatureFlagProviderRegistrythat implementsFeatureFlagSnapshotSourceand merges the results in registration order; the first provider that knows a key wins. A provider that throws is skipped, so one unreachable vendor does not hide the others. See The Feature Flags tool. build_prepare.yamlswapsapp_debugger_feature_flag→app_debugger_feature_flag_noopfor production-flavor builds, stripping the spoofer entirely.
The Feature Flags tool
- List: flags grouped by source (Remote, Cached, Default). Each row shows the key, an Overridden badge, the source (
remote,cache,default) and the value with its type (bool,number,text,json). The type is the provider'sFlagSnapshot.valueType. For a key no provider reports, it is inferred from the override. - Search matches keys and values. Filter chips: All, Overridden, Remote (includes cached values), Default, each with a count.
- Bool flags have a switch in the row. So does a JSON object with exactly one bool field, such as
{"enabled": true}: the switch toggles that field. The switch shows the value the app gets. Switching back to the provider's value removes the override, so a flag is either remote or overridden, never stale. A paused or reset override shows the provider value. - Other flags open a full-screen editor: the key as the title with a copy button, Save in the app bar, and Reset to remote value at the bottom, off when the flag is not overridden. JSON is pretty-printed in a monospace editor with a Format button. An invalid document shows the parse error with its line and column below the field, and Save stays off. Numbers get a numeric field, text a multiline field. Saving an unchanged value shows "No changes".
- Spoofer switch: off keeps every override but stops applying it. The list shows provider values and is view-only: editing shows "Spoofer is off".
- Reset per flag and Reset all, each with Undo. Fetch now calls
fetchNow()on every provider and lists the flags again. - Persistence: overrides and the spoofer switch are saved to
feature_flag_overrides.jsonin the app support directory and restored ininitialize(), before the first flag evaluation. Without a file system (unit tests) they stay in memory.


Match your design system: SpooferUiKit
The tool draws every control through SpooferUiKit, a port in app_debugger_feature_flag_api. The page builds only layout (rows, columns, padding); every visible control comes from the port.
| Method | Draws |
|---|---|
openPage(context, builder) | Pushes the full-screen editor |
page(context, title:, actions:, body:, bottom:) | The editor page: app bar with title and actions, body, pinned bottom action |
showMessage(context, message, actionLabel:, onAction:) | Short feedback, with Undo |
searchField(context, hint:, onChanged:) | The search field |
textField(context, controller:, label:, errorText:, multiline:, monospace:, keyboardType:, onChanged:) | The editor field, JSON included |
filterChip(context, label:, selected:, onSelected:) | One single-select filter |
toggle(context, value:, onChanged:, semanticLabel:) | Row switches and the spoofer switch |
pill(context, text:, label:, tone:) | Source, typed value and the Overridden badge (SpooferPillTone.highlight) |
sectionHeader(context, label) | A source group header |
note(context, text) | Counts, hints, empty state |
flagTile(context, title:, badges:, onTap:, trailing:) | One flag row |
button(context, label:, onPressed:, kind:) | SpooferButtonKind.primary, secondary or destructive |
iconButton(context, icon:, tooltip:, onPressed:) | Fetch now, reset, copy |
loadingBar(context, loading:) | The divider that shows fetch progress |
Without a registration the page uses KitSpooferUiKit, generated into app_debugger_feature_flag: your UI kit's text field for search, kit tokens (context.colors, context.spacing, context.typography, context.radius) for pills, headers and notes, and Material styled by your kit theme for the rest. It uses only the kit widgets and tokens the debugger home already uses, so it compiles wherever app_debugger does.
Write your own adapter
Extend MaterialSpooferUiKit and override only what your design system draws differently, then register it in the root GetIt before the debugger opens:
class MySpooferUiKit extends MaterialSpooferUiKit {
const MySpooferUiKit();
@override
Widget filterChip(
BuildContext context, {
required String label,
required bool selected,
required VoidCallback onSelected,
}) => MyPill(label: label, selected: selected, onTap: onSelected);
@override
Widget toggle(
BuildContext context, {
required bool value,
required ValueChanged<bool> onChanged,
required String semanticLabel,
}) => MySwitch(value: value, onChanged: onChanged, label: semanticLabel);
}
// In your app's DI setup:
getIt.registerSingleton<SpooferUiKit>(const MySpooferUiKit());Generation adds the _api package to your app's pubspec.yaml, so the app can import it.
Production-flavor builds keep the _api package, so the registration compiles in every flavor. Return widgets that take their colors from your theme, and keep tap targets at 48dp or more.
Adapter notes:
- If your kit's button awaits haptic feedback before it calls
onPressed, widget tests that tap it hang. Turn haptics off in tests, or do not await them. - The pill text is already clipped to one short line (about 32 characters, ending in an ellipsis) by the page, so your pill does not need to ellipsize. The full value stays in the editor.
MaterialSpooferUiKit.pageadds trailing room after the app bar actions, so Save does not sit in the screen corner under the debug banner. A kit that overridespageshould do the same.
DI and wiring notes
app_debugger_feature_flag/pubspec.yaml declares an archipelago: manifest block:
archipelago:
di_modules:
- class: AppDebuggerFeatureFlagPackageModuleso monorepo_toolkit generate host-registry can rediscover and re-wire the module after the initial post_gen pass. FeatureFlagSpooferSDKImpl is registered as an eager @Singleton (a lazy one would join FeatureSDKRegistry too late and the Feature Flags tool would never appear). post_gen.dart also patches the archipelago:debug_panel_pages sentinel in app_debugger's own di/debug_tools.dart (not the host app's bootstrap.dart — moved there in 1.1.1) to resolve it before first frame; verify it's present if the tool is missing.
post_gen.dart also appends a second, dedicated archipelago:feature_flag_spoofer_delegate sentinel right after the debug_panel_pages block, registering the delegate as shown in the API section above — so both the panel tab and the override delegate are wired automatically on generation, with no follow-up step in your own app code.
The noop side (app_debugger_feature_flag_noop) is hand-written directly in injector.dart with a matching AppDebuggerFeatureFlagPackageModule class name and an empty init() — as of 1.1.2 there is no codegen and no FeatureFlagSpooferNoopSDKImpl class, since FeatureFlagSpooferSDK is only ever resolved from inside app_debugger's own production-stripped composition.
Dependencies
app_debugger_api(path dependency on the siblingapp_debuggerbrick's API package)feature_flag_api—FeatureFlagDelegate/FeatureFlagResultcontracts and theFeatureFlagSnapshotSource/FlagSnapshotlisting contractfeature_sdk,identifiers,router_registry,dependencies— shared packages- Your UI kit package (
uiKitPackageName), forKitSpooferUiKit path_provider, to save overrides in the app support directoryapp_debuggeris generated first when the project lacks it.
Known Limitations
- A provider that does not implement
FeatureFlagSnapshotSourceadds no flags to the list, and the tool cannot override them. Override such a flag throughFeatureFlagSpooferSDK.setOverride. - The labels are English. The debugger tools are not localized yet.
- A bool switch cannot pin a flag to the value the provider already returns: switching to the provider value removes the override.
- After Fetch now,
FeatureFlagManagercan return a cached result for up to its cache TTL (5 minutes by default). di/debug_tools.dartis the target on a current project. The marker moved betweenapp_debuggerversions, so the hook uses the first file that has it:app_debugger'slib/src/di/debug_tools.dart(2.0.0 and later), itslib/src/app_debugger_sdk_impl.dart(1.3.0 to 1.15.x), then the app'slib/bootstrap.dart(older), and adds the imports and path dependencies to that package. A file that already resolves the SDK is left as it is. If no file has the marker,pre_genprints the lines to add and stops before it writes any file.
Changelog
Run archipelago brick info app_debugger_feature_flag to see the latest published version. Enterprise customers can read the full CHANGELOG.md in the brick source repository.