Edge evaluation
f69’s evaluator is a small Rust function that compiles to native or
WebAssembly. Given a manifest, an entity context, and now_utc
(evaluation instant), it returns a JSON value per flag (null, boolean,
number, string, array, or object). The rules in this page are the frozen public
contract: SDKs that re-implement evaluation client-side must reproduce them
exactly, especially the bucket hash. Legacy manifests that only used booleans
for default_value and outcomes behave the same on the wire (true / false).
f69-edge uses the server clock (Utc::now()) for each request. The public HTTP
API does not accept a client-supplied time for now_utc. The Cloudflare
WebAssembly entrypoint takes an explicit evaluated_at (RFC 3339) in the
request body so the host and tests are unambiguous.
Manifest shape
Section titled “Manifest shape”A manifest is the per-environment snapshot of flags and segments plus a version string:
{ "schema_version": 6, "manifest_version": "live", "project": "default", "environment": "prod", "flags": [ /* Flag */ ], "segments": [ /* Segment */ ]}Each flag carries a default_value (any JSON) and an ordered list of rules.
How a flag resolves
Section titled “How a flag resolves”Pseudocode:
evaluate(flag, ctx, now_utc) = for rule in flag.rules: if rule.when.all(hold_on(ctx, now_utc)): return resolve(rule.outcome, ctx) return flag.default_value- Rules are evaluated in manifest order.
- The first rule whose
whenpredicates all hold wins. - An empty
whenlist matches unconditionally (catch-all rule). - Predicates inside
whenare ANDed at the top level; useand/or/notfor nested logic. Predicate results are always boolean; only the flag payload (default_value, fixedoutcome, or rollout variantvalue) is arbitrary JSON.
POST /v1/evaluate response
Section titled “POST /v1/evaluate response”Each item in the JSON array includes key, value (any JSON), reason
(TARGETING_MATCH, SPLIT, DEFAULT, FALLBACK, or ERROR), and version
(the manifest version string). Treat value as a generic JSON value in your
SDK, not strictly a boolean.
Predicates
Section titled “Predicates”All predicates are JSON-tagged by op (snake_case) and read from
context.attributes[key] or context.id / context.type.
op | Shape | True when |
|---|---|---|
eq / neq | {key, value} | JSON equality / inequality |
in / not_in | {key, values[]} | attr matches any / no member of values |
gt / gte / lt / lte | {key, value} | numeric comparison via as_f64 |
entity_id_in | {values[]: string} | context.id is in values |
entity_type_eq | {value: string} | context.type == value |
and / or | {predicates[]} | every / any child holds (empty and is true; empty or is false) |
not | {predicate} | inner predicate is false |
in_segment | {segment: string} | context is a member of the named segment (segment membership also uses now_utc for time predicates) |
bucket | {by, seed, range: [u32, u32]} | bucket(seed, by, ctx) is in the inclusive [lo, hi] basis-point range |
before_instant | {at: string} (RFC 3339) | now_utc < at (strict) |
after_instant | {at: string} (RFC 3339) | now_utc >= at (inclusive) |
local_time_windows | {timezone, windows[]} | IANA timezone; each window has weekdays (0=Sun … 6=Sat) and start / end as HH:MM; true if any window matches in local time; empty list is false |
A missing attribute makes attribute predicates evaluate to false -
predicates fail closed, never silently true.
Rollouts
Section titled “Rollouts”A rule whose outcome is a rollout hashes the context into a basis-point bucket (0 - 9999) and picks a variant by cumulative weight:
{ "type": "rollout", "by": { "kind": "entity_id" }, "seed": "new_checkout", "variants": [ { "weight": 2500, "value": true }, { "weight": 7500, "value": false } ]}Each variant’s value may be any JSON (same as a fixed value outcome), not
only booleans.
Rules:
- Variant weights are basis points and must sum to exactly 10 000.
- Raising a variant’s weight pulls more entities across the line without reshuffling the ones already selected. Ramps are sticky.
by.kindisentity_id(most flags) orattribute(cohort stickiness - everyone in the same workspace flips together).
Phased or scheduled exposure: a static rollout (weights in the manifest) plus
republish to change weights is the sticky ramp. Time predicates in when only
window which rules apply; they do not interpolate a new percentage on every tick
in one published manifest. A future clock-driven percentage ramp in a single publish
is a separate design.
The bucket hash (frozen)
Section titled “The bucket hash (frozen)”The bucket hash is a public, byte-for-byte contract. Changing it would silently reshuffle every live rollout.
- Hash: SipHash-1-3 (1 compression round, 3 finalization).
- Key: 128-bit zero key (
[0u8; 16]). No per-deployment secret. - Input: UTF-8 of the canonical string below.
- Bucket:
(digest % 10_000) as u32.
HashBy | Canonical input string |
|---|---|
entity_id | "{seed}:{context.type}:{context.id}" |
attribute { key } (present) | "{seed}:{stringified_attr_value}" |
attribute { key } (missing) | "{seed}:" |
Stringification rules for attribute values:
null→ empty stringbool→"true"/"false"string→ the string verbatim (no extra quotes)- numbers / arrays / objects → compact JSON (no whitespace)
Pinned golden examples:
| seed | type | id | bucket |
|---|---|---|---|
new_checkout | user | u-alice | 1682 |
new_checkout | user | u-bob | 5811 |
other_flag | workspace | ws-42 | 9570 |
Seed guidance
Section titled “Seed guidance”- Default to the flag key as the seed, so two concurrent 10 % rollouts target different 10 % cohorts of users.
- To reshuffle everyone after a botched launch, change the seed
(
"{flag_key}:v2"). - To coordinate cohorts across flags, share the seed.
Segments
Section titled “Segments”A segment groups entities reusably:
{ "key": "beta-testers", "included": [ { "type": "user", "id": "u-alice" } ], "excluded": [ { "type": "user", "id": "u-banned" } ], "rules": [ [ { "op": "eq", "key": "is_internal", "value": true } ], [ { "op": "eq", "key": "country", "value": "NG" }, { "op": "gte", "key": "account_age_days", "value": 30 } ] ]}Membership order:
- If
(type, id)is inexcluded→ not a member. - Else if it’s in
included→ member. - Else if any of
rulesmatches → member. - Otherwise → not a member.
Segments may reference other segments via in_segment, but the reference
graph must be a DAG.
Error model
Section titled “Error model”evaluate_flagpropagatesUnknownFlag(key)to the caller.evaluate_allis fail-closed: if a single flag errors, the runtime returns that flag’sdefault_valueand keeps going. One bad flag cannot break the rest of the manifest.- Compile-time errors (
UnknownSegment,DuplicateSegment,SegmentCycle,RolloutInvalid,TimePredicateInvalid) are caught when the manifest is prepared, not on the hot path.
The bias is always toward default_value; an evaluation error never
replaces a resolved value with an arbitrary substitute beyond that fallback.