A feature flag lets your program choose behavior using configuration. Your code asks for a decision, receives a typed value, and uses it in an existing code path. Publishing a flag revision changes that decision without rebuilding the program for every configuration change.
The feature's implementation must already exist in the program. A flag also does not authorize a user: keep the server's access checks in place whichever variation is selected.
#Understand a decision
A flag has named variations, such as off and on. Their values might be booleans, strings, numbers, or JSON. The evaluator selects a variation using the flag's enabled state, ordered rules, and rollout configuration.
The caller supplies a fallback for cases where no usable decision is available, and contexts describing the subject of evaluation. A context has a kind and stable key, such as an organization ID. Derive security-relevant context from verified backend state instead of trusting arbitrary browser attributes.
#Evaluation order
- If the requested flag is absent, use the supplied fallback.
- If the flag is disabled, select its off variation.
- Evaluate rules in order. A matching rule with a usable result selects that result.
- If no rule selects a result, try the flag's rollout when present.
- Otherwise select the default variation.
A rollout missing its required context or attribute cannot select a bucket, so evaluation continues to the next applicable result. Inspect the decision reason and revision instead of guessing why a value was returned.
#Stable rollout is not a coin toss
A rollout assigns a stable subject to one of 100,000 buckets. Namespace, flag identity, salt, context kind, and selected context value participate in that assignment. Keep identities and salt stable when adjusting allocations if you want subjects to retain consistent assignments.
An allocation change can still move subjects between variations. A missing context should not be replaced with a random identifier on each request.
#Configure evaluation
#Enable the runtime client
Add the capability to an existing runtime in fluffy-chainsaw.yaml:
runtimes:
api:
uses:
flags: trueFluffy derives the flag namespace from the workspace foundation, application, and environment. There is no authored namespace or per-flag action list under uses.flags.
#Add a local flag
In fluffy-chainsaw.local.yaml beside the application manifest:
apiVersion: fluffy-chainsaw.dev/local/v1
flags:
revisionId: local-1
flags:
- id: new-layout-id
key: new-layout
salt: new-layout-stable-salt
enabled: true
offVariationId: "off"
variations:
- id: "off"
value: {boolean: false}
- id: "on"
value: {boolean: true}
defaultVariationId: "on"Preserve any other local flags, fixtures, and seeds. This definition has no rules, so an enabled flag selects on and a disabled flag selects off. The names are labels; their actual boolean values are explicit.
#Add a gradual rollout
To select on for 10% of organization buckets and off for the rest, add this to that flag:
rollout:
contextKind: organization
allocations:
- {variationId: "on", buckets: 10000}
- {variationId: "off", buckets: 90000}All allocations must reference existing variations and sum to 100,000. This uses the organization context's key. An optional attribute selects a context attribute instead. These are deterministic bucket proportions, not a promise that precisely 10% of a small group of organizations will be enabled.
#Evaluate on the backend
Generate the API's SDK for your application's name; this example uses example-app:
fluffy-chainsaw generate --application example-app --runtime api --language typescriptUse the sidecar setup for your selected language. These examples use example-app and module example.com/app; adjust the imports to your application. TypeScript uses address and transport; Python uses channel. Go passes its connection and a deadline-bearing context to the function:
import { Metadata } from '@grpc/grpc-js'
import * as runtimev1 from './.fluffy/generated/runtimes/api/typescript/fluffy/runtime/example-app/api.js'
import * as flagsv1 from './.fluffy/generated/runtimes/api/typescript/fluffy/flags/v1/flags.js'
const client = new runtimev1.FlagsClient(address, transport)
const decision = await new Promise<flagsv1.EvaluateResponse>((resolve, reject) => {
client.evaluate(
flagsv1.EvaluateRequest.fromPartial({
flagKey: 'new-layout',
fallback: { boolValue: false },
contexts: [{ kind: 'organization', key: 'demo-organization' }],
}),
new Metadata(),
{ deadline: Date.now() + 5000 },
(error, value) => {
if (error) reject(error)
else resolve(value)
},
)
})
const newLayoutEnabled = decision.value?.boolValue ?? falseUse the result in your handler and keep the fallback compatible with the expected value type. Replace the demonstration organization key with verified application context. EvaluateBatch obtains several decisions together.
For a browser feature, return the needed decisions and safe operational metadata from your backend. Do not ship private targeting rules or context attributes just to let the browser repeat the evaluation.
#Verify local changes
Run locally and request a decision. Switch defaultVariationId from on to off while no rollout or rule overrides it, then request a fresh decision. The local sidecar watches the snapshot file. An invalid update retains the last valid snapshot and reports unhealthy state; without a valid snapshot, evaluation uses the caller's fallback.
Test an unknown flag key, a disabled flag, and a rollout with missing context. Check the reason/revision as well as the value. Those cases explain different branches of evaluation and should not all be described as a matching rollout rule.
#Publish cloud changes
Deploy the flags-enabled application and shared foundation through Deployment. Core owns the management service, evaluator, and standalone flag configuration UI. Edit a draft, validate it, publish a revision, and inspect the revision reported by backend decisions. Use revision history for rollback.
Premium's Control Room embeds that same configuration experience and can add configured exposure analytics. It does not introduce a second configuration store or evaluator.
#Reference: snapshot fields and constraints
| Field | Meaning and constraint |
|---|---|
revisionId | Identifies the local snapshot revision |
Flag id, key, salt | Required nonempty identifiers; flag IDs and keys must be unique |
enabled | Selects normal evaluation or the off variation |
variations | Unique variation IDs with exactly one typed value each |
offVariationId, defaultVariationId | Must reference existing variations |
rules | Ordered rules with unique IDs, nonempty clauses, and exactly one variation or rollout result |
rollout.contextKind | Required context kind used for bucketing |
rollout.attribute | Optional attribute; otherwise the context key is used |
rollout.allocations | Positive bucket counts referencing variations; total exactly 100,000 |
Variation values contain exactly one of boolean, string, integer, double, or json. Local JSON is authored as a JSON string. Clause scalars also support timestamps.
#Rule operators
equals, notEquals, oneOf, and exists test attributes. Numeric comparisons use lessThan, lessThanOrEqual, greaterThan, and greaterThanOrEqual. Timestamp comparisons use before, beforeOrEqual, after, and afterOrEqual.
A clause names contextKind, attribute, and operator. exists takes no operand. oneOf takes a nonempty values list. Other operators take one value. Missing attributes do not satisfy a clause; notEquals does not make an absent attribute match. All clauses in a rule must match.
#Decision metadata and failures
Decision reasons distinguish DISABLED, RULE, ROLLOUT, DEFAULT, and FALLBACK. A missing generated client is a capability/generation problem. A fallback needs the requested key, usable snapshot, and expected type checked. A surprising rollout needs stable context, salt, allocations, and revision checked.
Keep operational fallback separate from business authorization. Continue with Run locally for snapshot reloads and the rest of the local lifecycle.