Feature Flags
The central place for builders to create feature flags, with an evaluation engine that determines which users see which features. Supports gradual rollouts and A/B experiments tied to event tracking.
Concepts
| Term | Definition |
|---|---|
| Flag | A named toggle or value that controls app behaviour. Has a key (e.g. new-checkout-ui), type (boolean/string/number/JSON), and default value. |
| Rule | A condition-based override for a flag. "If user is in group X, return true." Rules have priority — highest priority rule that matches wins. |
| Condition | A single check within a rule. "profile_id IN [abc, def]" or "country EQ GB". Multiple conditions in a rule are ANDed. |
| Rollout | A percentage-based gradual release. "Enable for 25% of users." Uses consistent hashing so a user always gets the same result. |
| Experiment | An A/B test linked to a flag. Defines variants, traffic split, and success metrics. |
Flag Types
| Type | Example Use | Default Value |
|---|---|---|
BOOLEAN | Enable/disable a feature | true / false |
STRING | Feature variant names, text overrides | "control" / "variant-a" |
NUMBER | Numeric thresholds, limits | 10 / 99.99 |
JSON | Complex configuration objects | {"maxItems": 5, "layout": "grid"} |
Rule Evaluation
When a flag is evaluated for a specific profile:
1. Get all rules for the flag, sorted by priority (highest first)
2. For each rule:
a. Evaluate all conditions (AND logic — all must match)
b. If conditions match:
- If rollout_percentage < 100:
- Hash(profile_id + flag_key) mod 100
- If hash < rollout_percentage → return rule's value
- Else → continue to next rule
- If rollout_percentage == 100 → return rule's value
3. If no rules match → return flag's default valueConsistent Hashing for Rollouts
The hash of profile_id + flag_key is deterministic:
- A user in the 25% rollout stays in the 25% (no flickering)
- Increasing from 25% to 50% adds new users without removing existing ones
- Different flags have independent hashes (being in 25% for flag A doesn't determine flag B)
Rule Conditions
| Attribute | Description | Example |
|---|---|---|
profile_id | Specific profile targeting | profile_id IN [abc, def] |
group_id | Target a group | group_id EQ engineering-team |
country | Geo-targeting | country IN [GB, US, DE] |
app_version | Version targeting | app_version GT 2.0.0 |
created_at | Account age | created_at GT 2026-01-01 |
custom.* | Custom attributes set by the app | custom.plan EQ premium |
Operators: eq, neq, in, not_in, gt, lt, gte, lte, contains, regex
Pre-Computed Flag Distribution
Feature flags are NOT evaluated on every request. Instead:
Flag created/updated
│
▼
Evaluation Engine
(background job)
│
▼
Compute resolved flags
for ALL affected profiles
│
▼
Store in DB:
{ profileId, appId } → { flagKey: value, ... }
│
▼
Available instantly via:
- GET /api/v1/flags/:appId/resolve?profileId=xxx
- Included in auth session data on next login/refresh
- SSE stream pushes update to connected clientsThis means:
- Flag reads are a simple DB lookup (fast, no rule evaluation at request time)
- Flag writes trigger background recomputation (async, can handle millions of users)
- The auth session includes resolved flags — apps don't need a separate API call
Experiments (A/B Testing)
An experiment links a feature flag to tracked events to measure impact:
Experiment: "New Checkout Flow"
├── Flag: new-checkout-ui (BOOLEAN)
├── Variants:
│ ├── Control (false) — 50% of users
│ └── Treatment (true) — 50% of users
├── Success Metric: checkout_completed event
├── Secondary Metrics: checkout_started, cart_abandoned
└── Duration: 14 daysThe experiment results dashboard shows:
- Conversion rate per variant
- Statistical significance (p-value)
- Confidence interval
- Sample size per variant
- Recommendation: "Treatment shows +12% conversion (p=0.003, significant)"
Usage in Code
// React SDK
const showNewCheckout = useFlag('new-checkout-ui', false);
const checkoutLimit = useFlag('checkout-limit', 100);
{showNewCheckout ? <NewCheckout /> : <OldCheckout />}// JS SDK
const showNewUI = exp.getFlag('new-checkout-ui', false);
const checkoutLimit = exp.getFlag('checkout-limit', 100);See the JS SDK and React SDK for full integration details.
API Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
POST | /api/v1/flags | Bearer | Create flag |
GET | /api/v1/flags | Bearer | List flags for app |
GET | /api/v1/flags/:id | Bearer | Flag detail + rules |
PUT | /api/v1/flags/:id | Bearer | Update flag |
DELETE | /api/v1/flags/:id | Bearer | Delete flag |
POST | /api/v1/flags/:id/rules | Bearer | Add rule |
PUT | /api/v1/flags/:id/rules/:ruleId | Bearer | Update rule |
DELETE | /api/v1/flags/:id/rules/:ruleId | Bearer | Delete rule |
GET | /api/v1/flags/:appId/resolve | API Key/Bearer | Get resolved flags for profile |
GET | /api/v1/flags/:appId/resolve/stream | API Key | SSE stream for real-time updates |
POST | /api/v1/experiments | Bearer | Create experiment |
GET | /api/v1/experiments/:id | Bearer | Experiment detail |
GET | /api/v1/experiments/:id/results | Bearer | Experiment results + stats |
See the full API Reference and Data Models for schemas.