Flags and rules
One primitive underneath everything: a typed value with rules. A feature flag is just the boolean case of remote config.
Types
A flag holds a boolean, a string, a number, or arbitrary JSON. The type is fixed when the flag is created, and every value it serves is checked against it: a number flag can never publish a string.
Environments
A flag is defined once for the project and configured separately in each environment. That split is what makes promoting a change from development to production a value change rather than a create.
Rules
Rules are ordered and matched top to bottom. The first rule whose conditions all match wins; anyone matching none gets the default. A rule with no conditions matches everyone who reaches it, which makes it a useful catch-all at the bottom.
iOS 2.4 and above, half of them -> true
Anyone whose email ends @aurora.app -> true
Otherwise -> falseAttributes
Conditions match against the context you pass to the provider. userId and deviceId are reserved; everything else comes from attributes. platform, appVersion, osVersion, country and locale are conventions, not special cases; you can match on anything you attach.
An attribute you did not send never matches, including with is not. Klor cannot prove a user is not on iOS when it was never told what they are on.
Percentage rollouts
A rule can apply to a share of the users who match it. Everyone else falls through to the next rule, so “roll out to 25% of iOS users” reads the way it sounds.
Bucketing is a stable hash of the flag key, a per-flag salt, and the attribute you bucket by. Three consequences worth relying on:
Stableper userThe same person lands in the same bucket on every launch and on every platform. Nobody flickers between variants.
Monotonicper rolloutRaising 10% to 20% only ever adds people. Anyone already seeing the feature keeps seeing it.
Independentper flagTwo flags at 10% hit different users, so the same unlucky tenth is not the only group ever seeing anything new.
A user with no value for the bucketing attribute falls through instead of being assigned at random; otherwise an anonymous user would get a different answer on every launch.
Splitting traffic between values
A rule normally serves one value. Give it variants instead and it splits the users it applies to across several, which is how you run an A/B test without a second system.
{
"id": "r_copy_test",
"conditions": [{ "attribute": "country", "op": "eq", "values": ["US"] }],
"rollout": { "percentage": 50, "bucketBy": "userId", "salt": "…" },
"value": "buy-now",
"variants": [
{ "value": "buy-now", "weight": 3 },
{ "value": "add-to-cart", "weight": 1 }
]
}Weights are relative, so they do not have to total 100. The pair above is a straight three-to-one split; adding a third arm with weight 1 makes it 60/20/20 without touching the others.
The rollout and the split answer different questions. The rollout decides whether a user is in the rule at all; the variants decide which value those users get. They are bucketed on different hashes, so widening a rollout from 10% to 50% never re-shuffles who is in which arm.
Assignment is stable: the same user gets the same arm on every launch and on every platform. Usage counters record which value was served, so the split shows up in the dashboard without sending anything about the user.
Sensitive flags
Marking a flag sensitive removes it from the payload public keys receive. This is the only confidentiality boundary Klor has: a public key ships inside your app and can be extracted, so everything else in that payload should be considered readable by anyone.