Remote Feature Flags
July 6, 2026 · View on GitHub
Overview
This document outlines the process for adding and managing remote feature flags in MetaMask (Extension and Mobile). Remote feature flags allow us to control feature availability and behavior in production without requiring a new release. They are created on the LaunchDarkly platform and distributed by our internal API ClientConfigAPI. Please read the Remote feature flags ADR for more details.
Choosing the Right Feature Flag Type
Choose the appropriate feature flag type based on your needs:
1. Boolean flag: Use when you need a simple ON/OFF toggle for a feature. In this example: my-feature is enabled for all users.
{
"name": "my-feature",
"value": true
}
2. Object flag with scope based on "threshold": Use for:
- controlling % of users who see a feature In this example: the feature is enabled for 30% of users and disabled for 70% of users.
[
{
"name": "feature is ON",
"scope": {
"type": "threshold",
"value": 0.3
},
"value": true
},
{
"name": "feature is OFF",
"scope": {
"type": "threshold",
"value": 1
},
"value": false
}
]
- A/B testing different feature variants In this configuration, the swap button will be blue for 25% of users, red for 25% of users, green for 25% of users, and yellow for 25% of users
[
{
"name": "blue",
"scope": {
"type": "threshold",
"value": 0.25
},
"value": "0000FF"
},
{
"name": "red",
"scope": {
"type": "threshold",
"value": 0.5
},
"value": "FF0000"
},
{
"name": "green",
"scope": {
"type": "threshold",
"value": 0.75
},
"value": "00FF00"
},
{
"name": "yellow",
"scope": {
"type": "threshold",
"value": 1
},
"value": "FFF500"
}
]
Threshold entries without thresholdVersion use the legacy output shape. After the controller selects the matching threshold entry, the processed flag is:
{
"name": "feature is ON",
"value": true
}
Use this shape when selectors read the selected flag data from .value.
Direct-value threshold entries
Use thresholdVersion: 2 when a threshold-based flag should expose the selected value directly, matching the shape of a non-threshold object flag. This is useful when converting an existing object flag to threshold rollout and existing selectors read root-level fields such as flag.enabled.
When using thresholdVersion: 2, use thresholdName instead of name to label the threshold entry. thresholdName is metadata for configuration readability and is not included in the processed feature flag value. Because the selected entry resolves directly to its value, this avoids confusion or collisions with any name field inside the value object itself. thresholdName is recommended for clarity, but the controller selects entries using scope and returns value.
[
{
"thresholdName": "feature is ON",
"thresholdVersion": 2,
"scope": {
"type": "threshold",
"value": 0.3
},
"value": {
"enabled": true,
"minimumVersion": "13.10.0"
}
},
{
"thresholdName": "feature is OFF",
"thresholdVersion": 2,
"scope": {
"type": "threshold",
"value": 1
},
"value": {
"enabled": false
}
}
]
If the first entry is selected, the processed flag is:
{
"enabled": true,
"minimumVersion": "13.10.0"
}
Existing non-threshold object flags do not need thresholdVersion. This property is only used on entries inside threshold arrays.
Explicit MetaMetrics ID targeting
For QA and Product Manager testing only. This mechanism is intended for targeting specific individuals in any environment, including production. It is not a general-purpose segmentation system and should not be used as a substitute for percentage rollouts.
Add an optional metaMetricsIds array to a threshold entry to pin specific users to that entry regardless of the hash-based rollout percentage. When the controller processes a threshold flag it checks each entry for an ID match before calculating the hash-based threshold. The first entry whose metaMetricsIds list contains the current user's MetaMetrics ID is selected immediately.
Precedence rules (highest to lowest):
- Local overrides (developer tools / test harness)
- Explicit
metaMetricsIdsmatch — first matching entry wins - Hash-based threshold selection (the normal rollout path)
Matching behaviour:
- IDs are compared case-insensitively after trimming whitespace from both sides.
- If multiple entries list the same ID, the first one in the array wins.
- Entries with a malformed
metaMetricsIdsfield (e.g. a string instead of an array) are silently skipped; that entry still participates in hash-based selection. metaMetricsIdsvalues are redacted from persisted state, state logs, and debug snapshots.
Example — combined explicit-ID targeting and percentage rollout:
The entry below pins two QA users to the treatment group. All other users are distributed between treatment (30%) and control (70%) by the usual hash-based rollout.
[
{
"thresholdName": "treatment — QA pinned + 30% rollout",
"thresholdVersion": 2,
"scope": {
"type": "threshold",
"value": 0.3
},
"metaMetricsIds": [
"f9e8d7c6-b5a4-4210-9876-543210fedcba",
"a1b2c3d4-e5f6-4789-abcd-ef1234567890"
],
"value": { "enabled": true }
},
{
"thresholdName": "control — remaining 70%",
"thresholdVersion": 2,
"scope": {
"type": "threshold",
"value": 1
},
"value": { "enabled": false }
}
]
- QA user
f9e8d7c6-...→{ "enabled": true }(explicit match, bypasses hash) - QA user
a1b2c3d4-...→{ "enabled": true }(explicit match, bypasses hash) - Any other user whose hash ≤ 0.3 →
{ "enabled": true }(hash-based, treatment) - Any other user whose hash > 0.3 →
{ "enabled": false }(hash-based, control)
The metaMetricsIds field can be added to any threshold entry, including legacy entries (without thresholdVersion). Explicit-ID matches do not affect or populate the threshold cache.
The distribution is deterministic based on the user's metametricsId, ensuring consistent group assignment across sessions.
3. Object flag with version-based scope: Use for:
- Rolling out features progressively across app versions
- Providing different feature configurations per version
- Maintaining backward compatibility while introducing new features
In this example: users get different UI configurations based on their app version, with the system selecting the closest lower or equal version match.
{
"newConfirmationsFeature": {
"versions": {
"13.0.0": { "ui": "legacy" },
"13.1.0": { "ui": "improved" },
"13.2.0": { "ui": "modern", "animations": true }
}
}
}
- v13.0.5 →
{ "ui": "legacy" }(closest lower or equal version) - v13.1.8 →
{ "ui": "improved" } - v13.2.1 →
{ "ui": "modern", "animations": true } - v12.9.0 → Feature excluded (no matching versions)
The version matching is deterministic and uses semantic version comparison to find the highest version that is less than or equal to the current app version.
thresholdVersion is not used for this flag type. The selected version value is returned as-is.
4. Composing version-based scope with threshold scope: Use when you need to control both version targeting and user percentage rollout simultaneously.
In this example: the feature is rolled out to 30% of users on v13.0.0+, and 100% of users on v13.2.0+.
{
"newFeature": {
"versions": {
"13.0.0": [
{
"thresholdName": "gradual rollout",
"thresholdVersion": 2,
"scope": {
"type": "threshold",
"value": 0.3
},
"value": { "enabled": true }
},
{
"thresholdName": "disabled",
"thresholdVersion": 2,
"scope": {
"type": "threshold",
"value": 1
},
"value": { "enabled": false }
}
],
"13.2.0": [
{
"thresholdName": "full rollout",
"thresholdVersion": 2,
"scope": {
"type": "threshold",
"value": 1
},
"value": { "enabled": true }
}
]
}
}
}
- v13.0.5 user in 30% bucket →
{ "enabled": true } - v13.0.5 user in 70% bucket →
{ "enabled": false } - v13.2.1 user (any bucket) →
{ "enabled": true }(100% rollout) - v12.9.0 user → Feature excluded (no matching versions)
When composing version-based and threshold scopes, place thresholdVersion: 2 inside each threshold entry under the relevant version. Without thresholdVersion: 2, the selected threshold entry uses the legacy { "name": "...", "value": ... } wrapper shape.
Implementation Guide
1. Creating feature flag in LaunchDarkly.
- Name your flag with team prefix (e.g.,
confirmations-blockaid,ramps-card) - Request LaunchDarkly access from TechOps. Here's a template email:
Subject: Requesting access to LaunchDarkly for [team]
Dear TechOps,
Can I please get access to LaunchDarkly with the "Metamask Extension and Mobile" role? (role key: `metamask-config`)
Thanks!
- Select appropriate project in LaunchDarkly:
- Extension: "MetaMask Client Config API - Extension"
- Mobile: "MetaMask Client Config API - Mobile"
- Create the flag following the LaunchDarkly flag creation guide
2. Using Remote Feature Flags in MetaMask clients
Extension
To use a remote feature flag in the MetaMask extension, follow these steps:
- Add selector from
import { getRemoteFeatureFlags } from 'path/to/selectors';in your component file. - Use the selector to get the feature flag value.
const { testFeatureFlag } = getRemoteFeatureFlags(state);
- Use the feature flag value in your component.
if (testFeatureFlag) {
// Use the feature flag value
}
Mobile
Create a selector for your feature flag
- Location:
app/selectors/featureFlagsController - Create a new folder with your feature flag name
- Follow the structure from
app/selectors/featureFlagsController/minimumAppVersion
Your selector must include:
- State selectors for each feature flag value
- Fallback values for invalid remote flag values
- TypeScript types
- Unit tests
- Any required business logic for value manipulation
Important
Always use the specific feature flag selector when accessing values. Accessing feature flags directly from Redux state or the main selector bypasses fallback values and can cause crashes.
3. Testing Remote Feature Flags
Extension
In production, MetaMask Extension fetches remote flags from the client-config API at runtime. During E2E tests, a global mock server (test/e2e/mock-e2e.js) reads from the feature flag registry instead of calling the real API. Each registry entry stores the flag's production default value, so tests reflect real-world behavior unless a specific test explicitly overrides a flag.
Local Feature Flag Override
- Developers can override
remoteFeatureFlagvalues by defining them in.manifest-overrides.jsonand enableMANIFEST_OVERRIDES=.manifest-overrides.jsonin the.metamaskrc.distlocally. - These overrides take precedence over values received from the controller
- Accessible in all development environments:
- Local builds
- Webpack builds
- E2E tests
Developer Validation
A. Local Build
- Define overrides in
remoteFeatureFlagsinmanifest-overrides.json:
{
"_flags": {
"remoteFeatureFlags": {
"testFlagForThreshold": {
"name": "test-flag",
"value": "121212"
}
}
}
}
- Verify in Developer Options:
- Open extension
- Click on the account menu (top-right corner)
- Select "Settings" > "Developer Options"
- Look for "Remote Feature Flags" section to verify your overrides
B. E2E Test
For detailed guidelines on handling remote (and build-time) feature flags in E2E tests — including the feature flag registry, override patterns, and general principles — see Feature flags in E2E tests.
Mobile
Local Feature Flag Override
- Set environment variable in
.js.envand run the app. This forces the fallback values to override any remote values.
OVERRIDE_REMOTE_FEATURE_FLAGS=TRUE
- Test different feature flag scenarios:
- Modify the default values in your feature flag selector
- Run the application to verify behavior with different values
- Remember to test both override and non-override scenarios