Feature Flags Plugin
The Feature Flags plugin lets you list, inspect, and force-override feature flags on a running device from React Native DevTools. It supports a homegrown custom store, LaunchDarkly, and Statsig through adapters.
Tier A vs. Tier B: read this before picking an adapter
Section titled “Tier A vs. Tier B: read this before picking an adapter”Whether overriding a flag needs a code change depends on who owns the override store:
- Tier A — the provider owns the store. The Statsig adapter points at a
LocalOverrideAdapteryou already construct for Statsig. No call-site change — overrides work as soon as the adapter is registered. The override lifetime is whatever thatLocalOverrideAdapterinstance’s lifetime is; this plugin doesn’t manage it. - Tier B — the plugin owns the store. The custom adapter and the LaunchDarkly adapter have no native override mechanism, so the plugin keeps its own in-memory override map. For LaunchDarkly this means passing a wrapped client to
<LDProvider>instead of the raw SDK client — the one line you have to change. Overrides are in-memory and reset on app restart unless you wire persistence yourself.
This is also why “Reset all overrides” in the panel is durable on Tier A and ephemeral on Tier B — restarting the app clears a Tier B reset’s effect but not a Tier A one. Neither is a bug.
Installation
Section titled “Installation”Make sure to go through the Getting Started guide before installing the plugin.
npm install -D @rozenite/feature-flags-pluginyarn add -D @rozenite/feature-flags-pluginpnpm add -D @rozenite/feature-flags-pluginbun add -D @rozenite/feature-flags-pluginInstall the peer dependency for whichever adapter you use:
npm install @launchdarkly/react-native-client-sdkyarn add @launchdarkly/react-native-client-sdkpnpm add @launchdarkly/react-native-client-sdkbun add @launchdarkly/react-native-client-sdknpm install @statsig/js-client @statsig/react-native-bindings @statsig/js-local-overridesyarn add @statsig/js-client @statsig/react-native-bindings @statsig/js-local-overridespnpm add @statsig/js-client @statsig/react-native-bindings @statsig/js-local-overridesbun add @statsig/js-client @statsig/react-native-bindings @statsig/js-local-overridesAdapter: Custom / local (Tier B)
Section titled “Adapter: Custom / local (Tier B)”For a homegrown flag store, or before wiring a real provider:
import { createCustomFlagsAdapter, useRozeniteFeatureFlagsPlugin,} from '@rozenite/feature-flags-plugin';
// Module-level, like storage/sqlite adapters elsewhere in the docs. The// hook tracks `providers` by content, so a fresh array literal on every// render works too -- hoisting just avoids rebuilding provider state for// nothing.const featureFlagsProviders = [ createCustomFlagsAdapter({ id: 'app', name: 'App flags', listFlags: () => flagStore.getAll(), }),];
function App() { useRozeniteFeatureFlagsPlugin({ providers: featureFlagsProviders });
return <YourApp />;}setOverride throws for a key not present in listFlags() — nothing is written for a typo’d or unknown key.
Overrides default to an in-memory Map. Bring your own store to persist them across restarts:
import { createFlagOverrides } from '@rozenite/feature-flags-plugin';
const overrides = createFlagOverrides({ initial: JSON.parse(storage.getString('flag-overrides') ?? '{}'), onChange: (all) => storage.set('flag-overrides', JSON.stringify(all)),});
createCustomFlagsAdapter({ id: 'app', name: 'App flags', listFlags, overrides });Adapter: LaunchDarkly (Tier B)
Section titled “Adapter: LaunchDarkly (Tier B)”createLaunchDarklyFlagsAdapter returns { provider, client }. Pass client — not your raw ReactNativeLDClient — to <LDProvider>. Every LD hook (useBoolVariation, useLDClient, …) reads through it from there automatically, because LD’s own hooks are a thin read off the context client.
import { ReactNativeLDClient, AutoEnvAttributes, LDProvider } from '@launchdarkly/react-native-client-sdk';import { createLaunchDarklyFlagsAdapter, useRozeniteFeatureFlagsPlugin,} from '@rozenite/feature-flags-plugin';
const rawClient = new ReactNativeLDClient(LD_MOBILE_KEY, AutoEnvAttributes.Enabled);const { provider, client } = createLaunchDarklyFlagsAdapter({ client: rawClient });const featureFlagsProviders = [provider];
function App() { useRozeniteFeatureFlagsPlugin({ providers: featureFlagsProviders });
return <LDProvider client={client}>{/* ... */}</LDProvider>;}Notes:
- Overrides are in-memory by default; pass
overrides: createFlagOverrides(...)to persist them, same as the custom adapter. - There is no
refresh()on this adapter — LD has no documented, version-stable way to force one client-side. - Anything holding a direct reference to the raw (unwrapped) client bypasses overrides.
Adapter: Statsig (Tier A)
Section titled “Adapter: Statsig (Tier A)”You construct StatsigClient and LocalOverrideAdapter yourself; the adapter only takes references.
import { StatsigClient } from '@statsig/js-client';import { LocalOverrideAdapter } from '@statsig/js-local-overrides';import { createStatsigFlagsAdapter, useRozeniteFeatureFlagsPlugin,} from '@rozenite/feature-flags-plugin';
const overrideAdapter = new LocalOverrideAdapter();const client = new StatsigClient(STATSIG_CLIENT_KEY, { userID: 'user-123' }, { overrideAdapter });await client.initializeAsync();
const featureFlagsProviders = [ createStatsigFlagsAdapter({ client, overrideAdapter, flags: [ { key: 'new-onboarding' }, // boolean gate (default type) { key: 'checkout-copy', type: 'string' }, { key: 'max-items', type: 'number' }, { key: 'layout-config', type: 'json' }, ], }),];
function App() { useRozeniteFeatureFlagsPlugin({ providers: featureFlagsProviders });
return <YourApp />;}Notes:
- Statsig has no client-side way to enumerate gates or dynamic configs, so you declare which flags exist via
flags. Reading or overriding an undeclared key throws. type: 'boolean'(the default) maps tocheckGate/overrideGate.- Dynamic configs hold a parameter map, not a single scalar.
string/numberflags read and write a single parameter namedvaluewithin the config by convention — this is a convention of this adapter, not part of the Statsig API.jsonflags read/write the config’s full parameter map instead. - There is no
refresh()— the closest SDK method re-identifies the user rather than refetching specs, so this adapter doesn’t invent one. - “Reset all overrides” only clears the flags declared in
flags, not every override in the sharedLocalOverrideAdapter— overrides for gates/configs this adapter doesn’t declare (e.g. from other debug tooling) are left alone.
Agent Tools (LLM Integration)
Section titled “Agent Tools (LLM Integration)”When this plugin is active, it registers agent tools under the @rozenite/feature-flags-plugin domain:
list-flags— list flags across providers, including type, effective value, and override state.get-flag— read a single flag by key.override-flag— force a flag to a value, useful for reproducing a bug report that only happens with a flag on.clear-overrides— clear one override or every override for a provider.
providerId is optional on every tool and resolves to the sole registered provider when there’s only one.
Once registered, a “Feature Flags” panel appears in DevTools showing every flag’s key, type, effective value, and whether it’s overridden. Flip a boolean with one click, edit strings and numbers inline, or open a dialog to edit a json flag. Overridden rows can be reset individually, or all at once from the toolbar (subject to the Tier A/Tier B lifetime difference above).
Next: Explore other Official Plugins or learn how to build your own in the Plugin Development guide.