Statsig is the standard provider. Services use the vendor SDKs directly, with no internal wrapper. Some services still use OpenFeature with LaunchDarkly. Treat these as legacy and move them to Statsig. The web reference implementation is ninja-web-trader/docs/guides/STATSIG.md. Follow it for the React surface.
- Ship dark and roll out gradually.
- Keep a kill-switch that fails safe.
- Remove the flag and dead branch when the rollout finishes.
Pick the right primitive
| Need | Use |
|---|---|
| A yes/no decision | Feature gate (boolean) |
| A value, scoped to one feature | Dynamic config |
| A value read across many surfaces | Parameter store |
| To measure the impact of a change | Experiment |
If the answer is “100 rows for free tier, 500 for paid,” use two values in a dynamic config, not two gates.
Access patterns
On the web, you can reach every primitive in two ways. Use the React hook inside a component. Use the singleton outside React, such as in async orchestrators, model files, and store actions.
import { useFeatureGate } from "@statsig/react-bindings";
function PortfolioPanel() {
const { value: showNewMetrics } = useFeatureGate("portfolio-new-metrics");
return showNewMetrics ? <NewMetricsView /> : <LegacyMetricsView />;
}// outside React — one-shot evaluation
const gate = StatsigClient.instance().getFeatureGate("web-trader-refresh-test-flag");Fail safe
The SDK already fails safe. A gate uses its default. config.get(key, fallback) returns the fallback when the key is missing or the SDK is still loading. Do not wrap the SDK to add this. The safe default is the accessor’s fallback argument.
const config = useDynamicConfig("order-ticket-defaults");
const maxQty = config.get("maxQuantity", 100); // fallback on miss or loadOn the backend, use the server SDK to evaluate the flag. If evaluation fails, use the proven path (illustrative):
// error -> safe default
def newRiskCheckEnabled(user: StatsigUser): Boolean =
Try(statsig.checkGate(user, "backend_new_risk_check")).getOrElse(false)
if (newRiskCheckEnabled(user)) newRiskCheck(order) else legacyRiskCheck(order)def new_risk_check_enabled(user: StatsigUser) -> bool:
try:
return statsig.check_gate(user, "backend_new_risk_check")
except Exception:
return False # error -> safe default
run_check = new_risk_check if new_risk_check_enabled(user) else legacy_risk_check
run_check(order)Flag types and lifespan
Name the type. Each type lasts for a different amount of time:
| Type | Purpose | Lifespan |
|---|---|---|
| Release | Ship dark, then ramp a new feature | Temporary, delete at 100% |
| Ops kill-switch | Disable a path under load or failure | Long-lived |
| Experiment | Measure a change before deciding | Until the experiment concludes |
| Permission / entitlement | Gate access by plan or role | Long-lived, enforced server-side |
A release flag is temporary by default. It has an owner and a removal date.
Rollout
Start at 0% or with an internal allowlist. Increase exposure by percentage or targeting rule. At each step, check the error rate and key metrics before increasing exposure. See observability. Keep the ability to return to 0% instantly. This is the kill-switch. Assign users by the stable userID, resolved up front, so each user sees consistent behaviour across the Scala, Node, and web surfaces.
Authorization
A feature flag controls what a user sees. It does not control what they are allowed to do. Enforce entitlement and access control on the server. Never use a client-delivered flag as the authorization boundary. See security.
Testing
While a flag exists, test both branches. Also test the fail-safe default when the SDK is unavailable. This prevents a flag flip from shipping an untested path. Never suppress an experiment’s exposure logging. It lets the experiment connect outcomes to the variant a user saw. See testing.
Cleanup
Cleanup is mandatory. When a rollout reaches 100%, open a cleanup ticket. Removing the flag deletes both the dead code branch and the gate definition in Statsig. No central key registry exists yet. If you read the same key in three or more places, extract a constant instead of repeating string literals. Keep logEvent metadata low-cardinality (symbol, side, tier, never order IDs or timestamps).
stateDiagram-v2 direction LR [*] --> Created Created --> Rollout: start at 0% or allowlist Rollout --> Ramp: increase exposure Ramp --> Rollout: kill-switch to 0% Ramp --> Full: reaches 100% Full --> Cleanup: delete branch and gate Cleanup --> [*]
References
ninja-web-traderSTATSIG guide, the canonical web integration.- Statsig React SDK and feature gates.
- Feature Toggles (Pete Hodgson), the toggle taxonomy and short-lived-flag case.
- The CI/CD phased-rollout bullet points here for surfaces that cannot switch server-side (installers, store releases).