Specification Document for GO Feature Flag OpenFeature Providers
| Specification Version | 1.0 |
| Creation Date | 03/08/2026 |
| Last Update Date | 03/08/2026 |
| Authors | Thomas Poignant |
| Minimum relay proxy | v1.55.0 |
| Evaluation engine | modules/core v0.7.2 (WASM 0.2.4) |
Overview
GO Feature Flag does not ship its own SDKs. Applications talk to it through OpenFeature SDKs plus a GO Feature Flag provider. Those providers exist in eight languages, written by different people at different times, and they have drifted apart — not only in naming and defaults, but in wire format, error semantics and evaluation results.
This document is the contract every server-side GO Feature Flag provider must satisfy so that the same flag, evaluated with the same context, returns the same result in every language.
It is written to be checked mechanically. Every normative statement carries a stable identifier, an RFC 2119 keyword and a severity, so an automated audit can walk a provider's source and emit a per-requirement verdict.
This specification covers server-side providers only. Client-side providers (Swift, Android, JavaScript Web) follow a different paradigm and are out of scope for version 1.0.
Relationship to other documents
- If you are writing a new provider, start with Implementing a GO Feature Flag Provider. It gives the component shape, build order and complete wire payloads that this document deliberately omits, and links back here for the rules. This document tells you what must be true; that one tells you what to build.
1. Scope and conventions
1.1 Requirement keywords
The key words MUST, MUST NOT, REQUIRED, SHOULD, SHOULD NOT and MAY are to be interpreted as described in RFC 2119.
1.2 Requirement identifiers
Every requirement has a stable identifier of the form GOFF-<AREA>-<NNN>. Identifiers are
never reused or renumbered; a withdrawn requirement is marked as such and its number retired.
1.3 Conformance tiers
Not every requirement applies to every provider. Each section declares a tier:
| Tier | Applies to |
|---|---|
| Core | Every server-side provider |
| Remote | Providers offering remote (OFREP) evaluation |
| In-process | Providers offering local evaluation |
| WASM | In-process providers that embed the engine through WebAssembly |
| Optional | MAY-level capabilities |
A requirement outside a provider's tiers is reported N/A, never FAIL. In particular, a provider written in Go links the evaluation engine directly and is N/A for the whole of §10 — it does not fail for declining to use WebAssembly.
Where the target language's OpenFeature SDK offers no equivalent capability at all — for example, an SDK with no Tracking API — the corresponding requirements are also N/A.
1.4 Severity
| Severity | Meaning |
|---|---|
| Critical | Wrong flag values, silent data loss, or unrecoverable provider state |
| Major | Observable misbehaviour, or a contracted capability that is missing |
| Minor | Telemetry fidelity, naming, cosmetics |
1.5 Deferring to the language SDK
Where the target language's OpenFeature SDK defines a behaviour — the canonical object type,
the ErrorCode enumeration, provider status transitions, event types, hook stage ordering —
the provider MUST follow its SDK. Those SDKs are implementations of one shared OpenFeature
specification, so deferring to them converges rather than diverges.
Where a divergence is a language accident rather than an SDK decision, this specification overrides it. Known accidents, called out so implementers know the override is deliberate:
- Python:
isinstance(True, int)isTrue, so a boolean silently satisfies an integer resolver. SeeGOFF-EVAL-004. - Java: a JSON number above
Integer.MAX_VALUEdecodes toLongand currently fails both the integer and the double resolver. - .NET:
GetInt32()throwsFormatExceptionon a non-integral number rather than producing a type mismatch.
1.6 Evaluation engine version
All providers MUST evaluate using the same engine build. Two providers on different engine versions can return different values for the same flag and context — a divergence no other requirement in this document would detect, because all of them would still pass.
| ID | Sev | Requirement |
|---|---|---|
GOFF-ENG-001 | Critical | The provider MUST evaluate using engine modules/core v0.7.2. A WASM-based provider satisfies this by pinning WASM module 0.2.4, which embeds that core version. |
GOFF-ENG-002 | Major | The pinned engine version MUST be recorded in a single machine-readable location in the provider repository (a version file, build property or dependency manifest). |
GOFF-ENG-003 | Minor | The provider SHOULD document which specification version it targets. |
Evidence for GOFF-ENG-001 is the pinned version declaration, not the binary itself.
Engine artefacts are commonly fetched at build time and absent from a source checkout, so an
audit verifies the pin and the code path that consumes it.
1.7 Delegated behaviour
A provider MUST be treated as accountable for the observable behaviour of any library it
delegates to, including a generic OFREP client. Deferring to a dependency is a legitimate
implementation choice, but it does not transfer conformance: if the delegate returns
TYPE_MISMATCH where this specification requires a value, the provider is non-conformant.
This does not conflict with §1.5. Deferring to the OpenFeature SDK for the shape of a type or the name of an error code is required. Inheriting a third-party client's behaviour where this specification is explicit is not.
An audit SHOULD record whether the remediation for such a finding lies upstream, since that materially changes who can fix it and how quickly.
2. Provider identity — GOFF-META
Tier: Core
| ID | Sev | Requirement |
|---|---|---|
GOFF-META-001 | Minor | The provider metadata name MUST be exactly GO Feature Flag Provider. |
GOFF-META-002 | Minor | The metadata name MUST be a literal constant. It MUST NOT be derived by runtime reflection on a class or type name, which is unstable under minification and obfuscation. |
GOFF-META-003 | Minor | Any provider name carried on emitted provider events MUST equal the metadata name. |
3. Configuration — GOFF-CFG
Tier: Core
Option names are RECOMMENDED, not normative: a provider SHOULD use the canonical name
adapted to its language's conventions — including a unit suffix where the language favours one,
such as flagChangePollingIntervalMs — and MAY retain an existing name as an alias.
Option semantics and default values are normative — they have real operational
consequences, and divergent defaults are how a fleet ends up behaving inconsistently.
3.1 Canonical options
| Canonical name | Type | Default | Applies to | Support |
|---|---|---|---|---|
endpoint | URL string | (required) | both | REQUIRED |
evaluationType | enum | in-process | both | REQUIRED |
apiKey | string | none | both | REQUIRED |
timeout | duration | 10000 ms | both | REQUIRED |
flagChangePollingInterval | duration | 120000 ms | in-process | REQUIRED |
exporterMetadata | map | empty | both | REQUIRED |
dataFlushInterval | duration | 60000 ms | both | REQUIRED |
maxPendingEvents | integer | 10000 | both | REQUIRED |
disableDataCollection | boolean | false | both | REQUIRED |
dataCollectorBaseURL | URL string | endpoint | both | RECOMMENDED |
evaluationFlagList | string list | empty (all) | in-process | RECOMMENDED |
wasmEvaluatorPoolSize | integer | CPU core count | WASM † | RECOMMENDED |
logger | SDK logger | language default | both | RECOMMENDED |
A REQUIRED option that is absent is a GOFF-CFG-003 failure. A RECOMMENDED option
that is absent is reported against its own requirement, not against GOFF-CFG-003.
† wasmEvaluatorPoolSize applies only where the runtime supports parallel execution. On a
single-threaded runtime it is N/A, for the reason given in GOFF-WASM-009.
3.2 Requirements
| ID | Sev | Requirement |
|---|---|---|
GOFF-CFG-001 | Major | endpoint MUST be required and validated at construction time. An absent or malformed value MUST raise a configuration error before any network activity. |
GOFF-CFG-002 | Major | The default evaluation mode MUST be in-process. |
GOFF-CFG-003 | Major | Every option listed in §3.1 that the provider supports MUST use the default value given there. |
GOFF-CFG-004 | Major | The provider MUST NOT mutate the caller's options object or any collection it contains. Normalisation MUST operate on a copy. |
GOFF-CFG-005 | Critical | The provider MUST NOT read environment variables to determine the endpoint or credentials. A feature-flag provider silently retargeting itself based on ambient environment is a security surprise. |
GOFF-CFG-006 | Major | dataCollectorBaseURL SHOULD be supported. Where supported it MUST override the base URL for the data-collector endpoint only — flag-configuration and evaluation requests MUST continue to use endpoint — and it MUST fall back to endpoint when unset. |
GOFF-CFG-007 | Major | Where supported, dataCollectorBaseURL MUST replace the whole base, including scheme, host, port and path prefix, and authentication, custom headers and timeout MUST apply to it identically. |
GOFF-CFG-008 | Minor | evaluationFlagList SHOULD be supported, and when non-empty MUST be transmitted as the flags array of the flag-configuration request. |
GOFF-CFG-009 | Major | An option that the provider declares and documents MUST be honoured. A declared option that is never read is a defect regardless of its default. |
GOFF-CFG-010 | Minor | The provider MUST NOT expose options for capabilities it does not implement. Vestigial options from removed features MUST be deleted rather than left inert. |
4. Lifecycle — GOFF-LIFE
Tier: Core
| ID | Sev | Requirement |
|---|---|---|
GOFF-LIFE-001 | Major | Initialization MUST block until the provider can serve evaluations, or fail. |
GOFF-LIFE-002 | Critical | Initialization MUST be safe to call more than once. A second call MUST cancel and join any existing polling task, MUST NOT start a duplicate one, and MUST NOT leak or double-instantiate the evaluation engine. |
GOFF-LIFE-003 | Critical | Any one-shot shutdown guard MUST be reset by re-initialization, so that a subsequent shutdown cannot hang or panic. |
GOFF-LIFE-004 | Major | Shutdown MUST stop the polling task, MUST flush all buffered events, and MUST stop the event publisher — in both evaluation modes and regardless of whether data collection is enabled. |
GOFF-LIFE-005 | Major | Shutdown MUST bound how long it waits for background work. |
GOFF-LIFE-006 | Critical | Until a flag configuration has been successfully loaded at least once, evaluations MUST report PROVIDER_NOT_READY. They MUST NOT report FLAG_NOT_FOUND, which misattributes an infrastructure failure to the caller's flag key. |
GOFF-LIFE-007 | Major | Where the runtime permits concurrent evaluation, it MUST be safe. Shared configuration state MUST be guarded, and the guard MUST NOT be held across network or evaluation calls. A single-threaded runtime, which has no such guards to hold, is N/A. |
5. Provider events and status — GOFF-EVT
Tier: Core
| ID | Sev | Requirement |
|---|---|---|
GOFF-EVT-001 | Major | Provider events MUST be emitted identically in both evaluation modes, for every event a mode can reach. A capability present in one mode and silently absent in the other is a defect. Remote evaluation holds no configuration of its own, so PROVIDER_CONFIGURATION_CHANGED and PROVIDER_STALE — which describe a cached snapshot changing or ageing — are N/A there. |
GOFF-EVT-002 | Major | PROVIDER_CONFIGURATION_CHANGED MUST be emitted when a poll yields a configuration different from the one in use. |
GOFF-EVT-003 | Major | PROVIDER_CONFIGURATION_CHANGED MUST NOT be emitted for the initial load during initialization. Consumers MUST NOT observe a configuration-changed event before the provider is ready. |
GOFF-EVT-004 | Major | PROVIDER_CONFIGURATION_CHANGED MUST NOT be emitted when the configuration is unchanged. A provider that cannot distinguish "changed" from "fetched" — for instance because the server sends no ETag — MUST compare content rather than emit unconditionally. |
GOFF-EVT-005 | Major | The provider SHOULD emit PROVIDER_STALE after 3 consecutive failed configuration refreshes, and MUST continue serving the last known-good configuration. |
GOFF-EVT-006 | Major | On recovery from stale, the provider MUST emit an event returning it to ready. |
GOFF-EVT-007 | Critical | Authentication failure (401, 403) during initialization MUST put the provider in PROVIDER_FATAL. Credentials cannot be repaired by retrying. |
GOFF-EVT-008 | Major | Every other initialization failure MUST put the provider in ERROR, not PROVIDER_FATAL, so it recovers unattended once the relay proxy is reachable. |
6. Evaluation API — GOFF-EVAL
Tier: Core
| ID | Sev | Requirement |
|---|---|---|
GOFF-EVAL-001 | Major | The provider MUST implement the boolean, string, integer, float and object resolvers defined by its SDK, and SHOULD implement asynchronous variants where the SDK defines them. |
GOFF-EVAL-002 | Critical | The object resolver MUST report TYPE_MISMATCH for any value its SDK's canonical structure type cannot represent, and MUST NOT coerce such a value. Where that type is a JSON-value union that would otherwise admit scalars, the provider MAY additionally reject them. |
GOFF-EVAL-003 | Critical | The float resolver MUST accept an integral JSON number. JSON does not distinguish 100 from 100.0. |
GOFF-EVAL-004 | Critical | A boolean value MUST NOT satisfy the integer or float resolver. It MUST report TYPE_MISMATCH. |
GOFF-EVAL-005 | Major | Where the SDK defines a distinct integer resolver, it MUST report TYPE_MISMATCH for a non-integral number, and MUST NOT truncate, round, or raise a raw numeric-conversion error. An SDK offering a single numeric resolver is N/A. |
GOFF-EVAL-006 | Critical | A null evaluation result MUST return the caller's default value, preserving the engine's reason, variant and metadata. It MUST NOT return the language's zero value. |
GOFF-EVAL-007 | Major | The reason MUST be passed through as an opaque string. The provider MUST NOT parse it into a closed enumeration. The engine emits TARGETING_MATCH_SPLIT, SPLIT, OFFLINE and others that a naive enum lookup will reject. |
GOFF-EVAL-008 | Major | A disabled flag MUST return the caller's default value with reason DISABLED and variant SdkDefault. |
GOFF-EVAL-009 | Major | Flag metadata MUST be passed through with its structure intact. Values MUST NOT be coerced to strings. |
GOFF-EVAL-010 | Major | Metadata keys added by the relay proxy, such as gofeatureflag_cacheable, MUST be passed through verbatim. The provider MUST NOT strip them, and MUST NOT require them. |
GOFF-EVAL-011 | Major | On any evaluation error the application MUST receive the caller's default value together with the error code. The provider MUST report the error through its SDK's contracted mechanism — in some SDKs that is raising a typed error the SDK itself catches — and MUST NOT raise an unmapped language-level exception. |
7. Evaluation context — GOFF-CTX
Tier: Core
| ID | Sev | Requirement |
|---|---|---|
GOFF-CTX-001 | Major | The targeting key MUST be transmitted under the key targetingKey. |
GOFF-CTX-002 | Major | Context attributes MUST be flattened alongside targetingKey, not nested under a wrapper. |
GOFF-CTX-003 | Critical | A missing or empty targeting key MUST be passed through to the evaluation engine. The provider MUST NOT reject it. The engine returns TARGETING_KEY_MISSING only for flags that actually require bucketing; rejecting client-side breaks flags that do not. |
GOFF-CTX-004 | Critical | In-process evaluation MUST normalise attributes exactly as the engine's own entry point does, including narrowing integral floating-point values to integers where the language distinguishes the two. Skipping this makes targeting rules match differently between languages for identical input. |
GOFF-CTX-005 | Major | (Tier: In-process.) evaluationContextEnrichment from the flag-configuration response MUST reach the evaluation, and enrichment wins on key collision. Handing it to the engine as flagContext.evaluationContextEnrichment (GOFF-IP-014) satisfies this — the merge itself belongs to the engine. In remote mode the relay proxy applies it and the provider is N/A. |
7.1 The gofeatureflag reserved namespace
The gofeatureflag context key is a shared namespace with three fields:
| Field | Written by | Purpose |
|---|---|---|
exporterMetadata | provider | Static metadata attached to exported evaluation events |
flagList | caller | Restricts which flags a bulk evaluation returns |
currentDateTime | caller | Overrides evaluation time, for testing scheduled rollouts |
| ID | Sev | Requirement |
|---|---|---|
GOFF-CTX-006 | Critical | The provider MUST merge into gofeatureflag, setting or replacing only exporterMetadata and preserving every sibling key. Replacing the whole object silently destroys caller-supplied flagList and currentDateTime. |
GOFF-CTX-007 | Major | exporterMetadata MUST be nested under gofeatureflag.exporterMetadata. Writing the metadata flat under gofeatureflag means the server never reads it. |
GOFF-CTX-008 | Major | If gofeatureflag is present but not a map, the provider MUST replace it rather than fail. |
GOFF-CTX-009 | Major | The provider MUST NOT write flagList or currentDateTime. They are caller inputs. |
8. Remote evaluation — GOFF-REM
Tier: Remote
Remote evaluation uses the OpenFeature Remote Evaluation Protocol.
| ID | Sev | Requirement |
|---|---|---|
GOFF-REM-001 | Major | Single-flag evaluation MUST be POST {endpoint}/ofrep/v1/evaluate/flags/{flagKey}. |
GOFF-REM-002 | Major | The request body MUST be {"context": { ... }} containing targetingKey and the flattened attributes. |
GOFF-REM-003 | Major | The response fields value, reason, variant and metadata MUST be mapped to the SDK's resolution details. variant on the wire corresponds to variationType in-process — they are the same concept. |
GOFF-REM-004 | Major | The provider MUST honour 429 responses by respecting Retry-After before issuing further requests. |
GOFF-REM-005 | Major | The provider MUST NOT implement any other retry behaviour. Polling and the caller's own retry policy are the recovery mechanisms. |
GOFF-REM-006 | Major | The configured timeout MUST apply to every remote request. The provider MUST NOT delegate to a transitive library's default. |
9. In-process evaluation — GOFF-IP
Tier: In-process
In-process evaluation is defined by behaviour, not by transport. A provider may embed the engine through WebAssembly or, where the language permits, link it directly. Both MUST produce identical results.
9.1 Flag configuration retrieval
| ID | Sev | Requirement |
|---|---|---|
GOFF-IP-001 | Major | The provider MUST fetch configuration with POST {endpoint}/v1/flag/configuration. |
GOFF-IP-002 | Major | The request body MUST be {"flags": [...]}, an empty array meaning "all flags". |
GOFF-IP-003 | Critical | Any path prefix on endpoint MUST be preserved. Building the URL from an absolute path discards it and silently retargets the request. |
GOFF-IP-004 | Major | The response flags and evaluationContextEnrichment MUST both be stored. |
GOFF-IP-005 | Major | The ETag response header MUST be stored verbatim, including surrounding quotes, and echoed verbatim as If-None-Match. The relay proxy issues strong validators; stripping quotes breaks the comparison. |
9.2 Polling and refresh
| ID | Sev | Requirement |
|---|---|---|
GOFF-IP-006 | Critical | The provider MUST poll for configuration changes on the configured interval. Polling MUST be active by default; it MUST NOT require explicit opt-in. |
GOFF-IP-007 | Critical | A 304 Not Modified response MUST NOT write flags, enrichment or timestamps — regardless of whether the response echoed an ETag header. The 304 path MUST be structurally incapable of carrying a configuration body: the transport layer MUST signal "not modified" by a distinct type or sentinel rather than by an empty response object, so that the distinction cannot be lost downstream. |
GOFF-IP-015 | Major | A 304 Not Modified response MUST NOT write the stored ETag. Writing back a value-identical validator is harmless in isolation, but it means the refresh path cannot distinguish a 304 from an empty 200, which is how GOFF-IP-009 is violated in practice. |
GOFF-IP-008 | Critical | A 200 response whose body cannot be parsed MUST be treated as a failed refresh: the previous configuration MUST be preserved and the stored ETag MUST NOT advance. |
GOFF-IP-009 | Critical | A 200 response whose decoded flag map is null or absent MUST likewise be treated as a failed refresh. Accepting it wipes every flag, and advancing the ETag makes the empty state permanent. A null evaluationContextEnrichment is not the same case and MUST be accepted as "no enrichment": the relay proxy builds that field from a Go map, and a nil map marshals to null. |
GOFF-IP-010 | Major | A failed refresh MUST NOT terminate polling. Polling MUST survive any error and continue on schedule. |
GOFF-IP-011 | Minor | The provider SHOULD apply jitter to the polling interval so that a restarted fleet does not poll in lockstep. |
GOFF-IP-018 | Minor | A provider MAY offer an explicit opt-out from polling. Where it does, the opt-out MUST be distinguishable from an unset interval, so that leaving the option alone polls at the default rather than disabling refresh. This is the companion to GOFF-IP-006, which forbids requiring opt-in. |
GOFF-IP-019 | Major | A configuration response whose Last-Modified is older than the one currently held MUST NOT replace it. An intermediary serving a stale copy would otherwise roll the configuration backwards. |
Make GOFF-IP-007 correct by construction rather than by null-checking. Have the HTTP layer
return a dedicated "not modified" sentinel instead of a response object, so the 304 branch is
structurally incapable of carrying a parseable body, and return from the refresh routine before
acquiring any lock on the configuration state.
9.3 Evaluation
| ID | Sev | Requirement |
|---|---|---|
GOFF-IP-012 | Major | A flag absent from the local configuration MUST yield FLAG_NOT_FOUND without invoking the engine. |
GOFF-IP-013 | Critical | Evaluation MUST be panic- and exception-safe. An engine fault MUST degrade to a GENERAL error and the caller's default value; it MUST NOT propagate into the application. |
GOFF-IP-014 | Major | The engine input MUST carry flagKey, flag, evalContext, and flagContext containing defaultSdkValue and evaluationContextEnrichment. |
GOFF-IP-016 | Critical | The flag configuration object MUST be treated as opaque. It MUST be handed to the engine exactly as received, with no field dropped, reordered by a lossy representation, or reconstructed from a typed model. The only field a provider MAY read is trackEvents. |
GOFF-IP-017 | Major | Unknown fields anywhere in the flag configuration response MUST be tolerated and preserved. A provider MUST NOT fail, warn, or discard on encountering a field it does not recognise. |
The evaluation engine owns the flag schema — variations, targeting, defaultRule,
percentage, scheduledRollout, experimentation, bucketingKey and whatever it gains next.
A provider that deserialises that schema into typed models silently drops any field added by a
newer engine, then hands the truncated flag to evaluation. The result is a wrong flag value
with no error — the failure mode this specification exists to prevent — and it appears the
moment the engine ships a feature the provider predates, without either side changing.
Passing the object through untouched is also considerably less code: a provider needs no flag model, no rollout types, no rule types, and no migration when the schema evolves.
10. WebAssembly ABI — GOFF-WASM
Tier: WASM. Providers that link the engine directly are N/A for this section.
The module exposes one entry point. The host writes a JSON request into linear memory, calls
evaluate, and reads a JSON response back out.
1. Serialize the input to UTF-8 bytes
2. ptr = malloc(byteLength + 1)
3. write bytes at ptr, followed by a NUL terminator
4. packed = evaluate(ptr, byteLength) // byteLength, NOT including the terminator
5. outPtr = (packed >> 32) & 0xFFFFFFFF
outLen = packed & 0xFFFFFFFF
6. read outLen bytes at outPtr and parse as JSON ← BEFORE any further call
7. free(ptr)
freeThe output buffer belongs to the module's garbage collector. It is pinned only until the
next call into the instance — and free is such a call. Freeing the input before reading
the output is a use-after-free: the read may return reclaimed memory, intermittently and
without error.
| ID | Sev | Requirement |
|---|---|---|
GOFF-WASM-001 | Major | The host MUST resolve the exports memory, malloc, free and evaluate, and MUST fail initialization if any is absent. |
GOFF-WASM-002 | Major | If _start is exported it MUST be invoked once per instance, and an exit code of 0 MUST be tolerated rather than treated as failure. |
GOFF-WASM-003 | Critical | The length passed to evaluate MUST be the UTF-8 byte length of the serialized input. Passing a string length measured in UTF-16 code units truncates the payload for any non-ASCII input. |
GOFF-WASM-004 | Major | The packed i64 result MUST be unpacked as pointer in the high 32 bits and length in the low 32 bits, using arithmetic wide enough not to overflow. |
GOFF-WASM-005 | Critical | The output MUST be read before any further call into the instance, including free. The output buffer is pinned only until the next call, so freeing first is a use-after-free. |
GOFF-WASM-006 | Major | The host MUST free the input pointer after reading the output, and MUST NOT free the output pointer — the guest owns the output buffer. A packed result of 0 means no output was produced and MUST be treated as an invalid result. |
GOFF-WASM-007 | Critical | The module is built with -scheduler=none and is not reentrant. One instance MUST serve one call at a time; a pool of instances is the RECOMMENDED way to get parallelism. |
GOFF-WASM-008 | Critical | If evaluation traps, the instance MUST be discarded and rebuilt. A trap does not unwind the module's shadow-stack pointer, so a trapped instance is permanently poisoned and MUST NOT be returned to a pool or reused. |
GOFF-WASM-012 | Critical | After a trap the host MUST NOT call free on the trapped instance. Running further code on it faults inside malloc at a wrapped address and masks the original error. |
GOFF-WASM-009 | Major | Where the runtime supports parallel execution, the instance pool SHOULD default to the host's CPU core count and SHOULD be configurable. A single-threaded runtime, on which calls into one instance cannot interleave, is N/A — GOFF-WASM-007 is satisfied there without a pool. |
GOFF-WASM-010 | Major | The engine binary version MUST be pinned to a single, machine-readable value. |
GOFF-WASM-011 | Minor | The provider SHOULD allow the binary path to be overridden, so that bundlers and non-standard packaging layouts remain usable. |
10.1 Built-in safeguards
Binaries after 0.2.3 carry a 1 MB shadow stack and return a structured PARSE_ERROR
instead of trapping when input exceeds a guard:
| Guard | Limit |
|---|---|
| Input JSON nesting depth | 128 levels |
| nikunjy query nesting | 64 brackets/parentheses |
| JSONLogic document nesting | 256 levels |
nikunjy [...] lists and JSONLogic operand arrays | 1,000 items |
nikunjy and/or conditions | 1,000 |
| nikunjy attribute path segments | 128 |
| ID | Sev | Requirement |
|---|---|---|
GOFF-WASM-013 | Major | The host MUST implement trap handling (GOFF-WASM-008, -012) regardless of which binary it bundles. The guards reduce traps but do not eliminate them, and older binaries carrying none of them remain in the field. |
A guard breach surfaces as PARSE_ERROR, which
§16 turns into a remote evaluation. That is the intended
outcome: the relay proxy evaluates on a full stack and has no equivalent limit, so a context
the embedded engine cannot handle still resolves correctly.
Input and output shapes are given in Appendix B.3.
11. Error model — GOFF-ERR
Tier: Core
11.1 Engine error codes
PROVIDER_NOT_READY, FLAG_NOT_FOUND, PARSE_ERROR, TYPE_MISMATCH, GENERAL,
INVALID_CONTEXT, TARGETING_KEY_MISSING, and the GO Feature Flag-specific FLAG_CONFIG.
11.2 Requirements
| ID | Sev | Requirement |
|---|---|---|
GOFF-ERR-001 | Major | Engine error codes with an SDK equivalent MUST be mapped to it. |
GOFF-ERR-002 | Major | Any unrecognised error code MUST map to GENERAL. It MUST NOT map to null, and MUST NOT raise an unmapped language-level exception. |
GOFF-ERR-003 | Major | errorDetails MUST be carried through as the error message. |
GOFF-ERR-004 | Major | HTTP failures MUST be distinguishable by status. 401 and 403 MUST be reported distinctly from 404, 400, 429 and 5xx. |
GOFF-ERR-005 | Major | Errors raised during background refresh MUST be logged and MUST NOT propagate to the application. |
GOFF-ERR-006 | Minor | Data-collector failures MUST be logged. Silently discarding them makes a permanently failing exporter undetectable. |
12. Hooks — GOFF-HOOK
Tier: Core
| ID | Sev | Requirement |
|---|---|---|
GOFF-HOOK-001 | Major | The provider's hooks MUST be observable by the time initialization completes, in the order [EnrichEvaluationContext, DataCollector]. |
GOFF-HOOK-002 | Major | Repeated initialization MUST NOT duplicate hooks. |
GOFF-HOOK-003 | Major | The enrichment hook MUST be registered unconditionally. Because exporterMetadata always contains the reserved keys of GOFF-COLL-010, it always has something to contribute. |
GOFF-HOOK-004 | Major | The enrichment hook MUST implement only the before stage, and MUST return a new context rather than mutating the caller's. |
GOFF-HOOK-005 | Major | The data-collector hook MUST implement the after and error stages. |
GOFF-HOOK-006 | Major | Both stages MUST honour disableDataCollection and the flag's trackability. Gating only one stage produces partial telemetry that looks like data loss. |
13. Data collection — GOFF-COLL
Tier: Core
Evaluation and tracking events are batched and posted to the relay proxy.
13.1 Envelope
{
"meta": { "provider": "python", "openfeature": true },
"events": [ ... ]
}
| ID | Sev | Requirement |
|---|---|---|
GOFF-COLL-001 | Major | Events MUST be posted to POST {dataCollectorBaseURL}/v1/data/collector. |
GOFF-COLL-002 | Critical | The metadata key MUST be meta. The events key MUST be events. |
13.2 Feature event
| ID | Sev | Requirement |
|---|---|---|
GOFF-COLL-003 | Major | kind MUST be feature. |
GOFF-COLL-004 | Critical | The "evaluation failed" boolean MUST be serialised as default. Any other name is silently discarded by the relay proxy, recording every failed evaluation as a success. |
GOFF-COLL-005 | Major | contextKind MUST be derived from the anonymous attribute per the table below. Where the attribute is present, only a boolean true yields anonymousUser; a truthiness test is not sufficient. |
GOFF-COLL-006 | Major | userKey MUST be the targeting key, or the sentinel undefined-targetingKey when absent. |
GOFF-COLL-007 | Major | creationDate MUST be Unix epoch seconds. |
GOFF-COLL-008 | Major | variation MUST be the resolved variant, or SdkDefault when none is available. |
GOFF-COLL-009 | — | Withdrawn. Number retired per §1.2. version remains part of the event schema and a provider MAY populate it, but none is required to. |
GOFF-COLL-010 | Minor | source MUST be INPROCESS for a locally evaluated flag, or PROVIDER_CACHE for a value served from a remote-mode cache. SERVER is reserved for the relay proxy. |
contextKind is decided as follows. The table is normative — it exists because a truthiness
test and an identity test agree on the common cases and diverge on the rest.
anonymous attribute | contextKind |
|---|---|
boolean true | anonymousUser |
boolean false | user |
| absent | user |
| evaluation context absent | anonymousUser |
| any non-boolean value | user |
The last-but-one row is the only one not decided by an identity test on anonymous, because there
is no attribute to read: with no evaluation context there is also no targeting key, so there is
nobody to attribute the evaluation to and anonymousUser is the honest bucket.
13.3 Exporter metadata
| ID | Sev | Requirement |
|---|---|---|
GOFF-COLL-011 | Major | The meta envelope MUST always contain provider and openfeature: true, whether or not the user configured any metadata. Without them events cannot be attributed to an SDK. |
GOFF-COLL-012 | Minor | provider MUST be a stable lowercase identifier naming the provider's runtime — python, java, dotnet, go, nodejs, android, php, ruby and rust are in use. The collector groups by it, so it MUST NOT change between releases. |
GOFF-COLL-013 | Major | exporterMetadata values MUST be restricted to string, boolean, integer or floating-point, and an invalid value MUST be rejected at construction time. |
13.4 Buffering and flushing
| ID | Sev | Requirement |
|---|---|---|
GOFF-COLL-014 | Major | Events MUST be flushed on the configured interval, when the buffer reaches maxPendingEvents, and on shutdown. |
GOFF-COLL-015 | Minor | The publisher MUST NOT flush immediately on start. There is nothing to send. |
GOFF-COLL-016 | Major | Flushing MUST be single-flight — concurrent publishes MUST NOT overlap. |
GOFF-COLL-017 | Critical | Single-flight MUST NOT be achieved by holding a lock across the HTTP call. Enqueuing an event runs inside an evaluation hook; blocking it couples flag-evaluation latency to data-collector availability. Swap the buffer under the lock, release, then post. |
GOFF-COLL-018 | Major | A failed batch MUST be re-queued preserving chronological order. |
GOFF-COLL-019 | Critical | The buffer MUST be capped at twice maxPendingEvents, discarding oldest events on overflow. An uncapped buffer is an unbounded memory leak during a collector outage. |
13.5 What is collected
| ID | Sev | Requirement |
|---|---|---|
GOFF-COLL-020 | Major | A flag whose configuration omits trackEvents MUST be treated as trackable. The engine's own default is true. |
GOFF-COLL-021 | Major | A flag absent from the configuration MUST be treated as trackable, so that a flag added between polls still produces data. |
GOFF-COLL-022 | Major | A flag whose configuration sets trackEvents: false MUST NOT produce feature events. |
GOFF-COLL-023 | Major | In remote mode, feature events MUST be emitted only for evaluations served from a provider cache. Uncached remote evaluations are already recorded by the relay proxy and MUST NOT be counted twice. |
14. Tracking — GOFF-TRACK
Tier: Core. N/A where the language's OpenFeature SDK has no Tracking API.
| ID | Sev | Requirement |
|---|---|---|
GOFF-TRACK-001 | Major | The provider MUST implement the SDK's Tracking API where one exists. |
GOFF-TRACK-002 | Major | Tracking events MUST be sent in both evaluation modes. The relay proxy does not synthesise custom events. |
GOFF-TRACK-003 | Major | Tracking events MUST honour disableDataCollection. |
GOFF-TRACK-004 | Major | kind MUST be tracking, and the details field MUST be trackingEventDetails. |
GOFF-TRACK-005 | Major | The event MUST carry evaluationContext, and MUST use the same contextKind, userKey and creationDate rules as §13. |
15. Authentication — GOFF-AUTH
Tier: Core
| ID | Sev | Requirement |
|---|---|---|
GOFF-AUTH-001 | Major | When apiKey is set, the provider MUST send X-API-Key: {apiKey}. |
GOFF-AUTH-002 | Major | Whatever authentication header the provider sends MUST be applied to every authenticated endpoint: flag configuration, evaluation and data collection. This is assessed independently of GOFF-AUTH-001 — a provider sending the wrong header consistently fails one requirement, not two, and the distinction tells a maintainer whether the fix is one line or several. |
GOFF-AUTH-003 | Major | When apiKey is unset or empty, no authentication header MUST be sent. |
GOFF-AUTH-004 | Minor | The provider SHOULD allow arbitrary additional headers, for deployments behind gateways requiring their own authentication. |
X-API-KeyThe relay proxy accepts both X-API-Key and Authorization: Bearer, resolving X-API-Key
first. X-API-Key is what the shipped providers send, so it is the one this specification
mandates. A provider currently sending Authorization: Bearer can therefore switch unilaterally,
with no coordinated server release and no breaking change for users.
16. Remote fallback — GOFF-FALLBACK
Tier: In-process
When local evaluation fails in a way that suggests the provider — rather than the flag — is at fault, the relay proxy is authoritative and reachable. Falling back to it converts a local failure into a correct answer.
| ID | Sev | Requirement |
|---|---|---|
GOFF-FALLBACK-001 | Major | When in-process evaluation returns raw engine code PARSE_ERROR or GENERAL, the provider MUST retry the evaluation remotely via OFREP and return the remote result. |
GOFF-FALLBACK-002 | Major | The trigger MUST be evaluated against the raw engine error code, before mapping to the SDK's error enumeration. |
GOFF-FALLBACK-003 | Major | FLAG_CONFIG MUST NOT trigger a fallback. It is a deterministic misconfiguration the relay proxy would reproduce identically. |
GOFF-FALLBACK-004 | Major | The fallback MUST be attempted on every qualifying occurrence. |
GOFF-FALLBACK-005 | Major | If the remote call also fails, the provider MUST return the original in-process error, and MUST log the remote failure. The in-process error is the root cause. |
GOFF-FALLBACK-006 | Major | A fallback result MUST NOT emit a feature event. The relay proxy has already recorded it. |
GOFF-FALLBACK-007 | Major | A fallback result MUST carry flag metadata gofeatureflag_evaluated_remotely: true. |
GOFF-FALLBACK-008 | Major | Each fallback MUST be logged at warning level. |
GOFF-FALLBACK-009 | Major | Authentication and timeout MUST apply to the fallback request identically to a normal remote evaluation. |
Where a fallback follows a WebAssembly trap, the ordering is: catch the trap, discard and
rebuild the instance (GOFF-WASM-008), then fall back. The provider MUST NOT retry locally
on the fresh instance first.
Because GOFF-FALLBACK-004 mandates fallback on every occurrence, a persistently malformed flag
turns every evaluation of it into a network round trip — an in-process provider silently
degrading to worse-than-remote latency. GOFF-FALLBACK-007 and GOFF-FALLBACK-008 exist so that
this condition is diagnosable rather than invisible.
17. Remote cache (optional)
Tier: Optional.
A provider MAY cache remote evaluation results locally. If it does, the following apply; if it does not, they are N/A and no feature events are produced in remote mode.
| ID | Sev | Requirement |
|---|---|---|
GOFF-CACHE-001 | Major | The cache MUST be an LRU with configurable maximum size and TTL, defaulting to 10000 entries and 60 s. |
GOFF-CACHE-002 | Major | A TTL of -1 MUST mean entries never expire. |
GOFF-CACHE-003 | Major | When present, the cache MUST be enabled by default and MUST be possible to disable by configuration. |
GOFF-CACHE-004 | Major | The cache key MUST combine the flag key and the evaluation context. |
GOFF-CACHE-005 | Major | Evaluation MUST consult the cache before calling the relay proxy, and MUST populate it on a miss. |
GOFF-CACHE-006 | Major | Expired entries MUST NOT be served, and the oldest entry MUST be evicted when the maximum size is reached. |
GOFF-CACHE-007 | Major | Cache hits MUST produce feature events with source: PROVIDER_CACHE — they are invisible to the relay proxy, which never saw the request. |
Appendix A — Endpoint reference
A.1 Normative
| Method | Path | Purpose |
|---|---|---|
POST | /ofrep/v1/evaluate/flags/{flagKey} | Remote single-flag evaluation |
POST | /ofrep/v1/evaluate/flags | Remote bulk evaluation |
POST | /v1/flag/configuration | In-process configuration fetch |
POST | /v1/data/collector | Evaluation and tracking events |
A.2 Superseded
These remain functional but MUST NOT be used by a conformant provider.
| Method | Path | Replaced by |
|---|---|---|
POST | /v1/feature/{flagKey}/eval | /ofrep/v1/evaluate/flags/{flagKey} |
POST | /v1/allflags | /ofrep/v1/evaluate/flags |
GET | /ws/v1/flag/change | /stream/v1/ws/flag/change (formally deprecated; the relay proxy returns RFC 8594 Deprecation headers) |
Appendix B — Conformance fixtures
These fixtures are canonical. They live in the go-feature-flag repository and providers MUST test against them unmodified. A provider whose local copy has diverged from canon is non-conformant regardless of whether its own tests pass.
B.1 Canonical evaluation context
{
"targetingKey": "d45e303a-38c2-11ed-a261-0242ac120002",
"email": "john.doe@gofeatureflag.org",
"firstname": "john",
"lastname": "doe",
"anonymous": false,
"professional": true,
"rate": 3.14,
"age": 30,
"company_info": { "name": "my_company", "size": 120 },
"labels": ["pro", "beta"]
}
B.2 In-process expected results
Source: openfeature/providers/python-provider/tests/mock_responses/config/valid-all-types.json,
evaluated with the context above. evaluationContextEnrichment is {"env": "production"}.
| Flag | Value | Variant | Reason | Error code |
|---|---|---|---|---|
bool_targeting_match | true | enabled | TARGETING_MATCH | — |
string_key | "CC0002" | color1 | STATIC | — |
double_key | 101.25 | medium | TARGETING_MATCH | — |
integer_key | 101 | medium | TARGETING_MATCH | — |
object_key | {"test":"false"} | varB | TARGETING_MATCH | — |
disabled_bool | SDK default | SdkDefault | DISABLED | — |
DOES_NOT_EXIST | SDK default | — | ERROR | FLAG_NOT_FOUND |
string_key (as boolean) | SDK default | — | ERROR | TYPE_MISMATCH |
Notes that catch real bugs:
string_keyresolves via its default rule with no targeting match, so the reason isSTATIC, notTARGETING_MATCH.string_keysetstrackEvents: falseand therefore MUST NOT produce a feature event (GOFF-COLL-022).- A disabled flag returns the caller's default with variant
SdkDefault— notdefaultSdk, and not a null variant.
B.3 Engine ABI vectors
Source: cmd/wasm/testdata/. Input:
{
"flagKey": "TEST",
"flag": {
"variations": { "enable": true, "disable": false },
"targeting": [
{
"name": "targetingID rule",
"query": "targetingKey eq \"random-key\"",
"percentage": { "enable": 90, "disable": 10 }
}
],
"defaultRule": { "variation": "disable" },
"metadata": { "description": "test flag", "type": "boolean" }
},
"evalContext": { "targetingKey": "random-key", "age": 42 },
"flagContext": {
"evaluationContextEnrichment": { "env": "production" },
"defaultSdkValue": false
}
}
Output:
{
"trackEvents": true,
"variationType": "enable",
"failed": false,
"version": "",
"reason": "TARGETING_MATCH_SPLIT",
"errorCode": "",
"value": true,
"cacheable": true,
"metadata": {
"description": "test flag",
"evaluatedRuleName": "targetingID rule",
"type": "boolean"
}
}
Two further vectors are canonical: an empty targeting key against a bucketing flag yields
errorCode: "TARGETING_KEY_MISSING" with variationType: "SdkDefault", and a malformed input
yields errorCode: "PARSE_ERROR" with value: null. Both MUST trigger the behaviour of
§16 where applicable.
B.4 Remote integration fixture
openfeature/provider_tests/flags.yaml drives the cross-language integration suites. Its
variations are named Default, "False" and "True"; the canonical context above matches the
targeting rule on every flag, so remote evaluation returns variant True. Each flag carries
metadata description and pr_link, which MUST be surfaced as flag metadata unmodified.
Appendix C — Conformance checklist and report format
C.1 Verdicts
Each requirement is reported as PASS, FAIL or N/A. N/A is reserved for
requirements outside the provider's declared tiers, or capabilities its SDK does not offer. A
requirement that applies but cannot be verified is FAIL, not N/A.
Vacuous satisfaction. A requirement whose precondition cannot occur because the governing
capability is absent MUST inherit the verdict of that capability's requirement. Without
this rule a provider that implements nothing accumulates free passes: every MUST NOT in a
section it has not built is trivially unviolated. For example, a provider with no fallback
path fails GOFF-FALLBACK-001, and GOFF-FALLBACK-003 and -006 — both prohibitions —
inherit that FAIL rather than passing.
Accidental satisfaction. A requirement met by coincidence rather than by intent MUST
be reported as PASS with a note. A provider that performs no trackability check at all
satisfies GOFF-COLL-020 and -021 while failing -022; recording the first two as unqualified
passes hides that the next refactor will break them.
C.2 Report format
A conformance report MUST contain four parts, in order:
- a header stating the specification version, the provider and version audited, and the
declared tier set — every
N/Amust trace back to it; - a single verdict line;
- findings grouped by severity, most severe first, each citing a requirement identifier and a source location;
- a per-requirement verdict table covering every identifier in the specification,
including passes, since
PASSwith a note is a meaningful outcome under §C.1.
GO Feature Flag Provider Specification 1.0 — <provider> <version>
TIERS: Core, Remote, In-process, WASM (Optional/§17: no cache → N/A;
§14: SDK has no Tracking API → N/A)
VERDICT: NON-CONFORMANT — 2 Critical, 5 Major, 3 Minor (118 PASS, 10 FAIL, 9 N/A)
CRITICAL
GOFF-IP-007 304 without ETag wipes the flag map src/evaluator/inprocess.ts:142
GOFF-CTX-006 Enrich hook replaces the gofeatureflag map src/hook/enrich.ts:18
MAJOR
...
PER-REQUIREMENT
GOFF-META-001 PASS src/provider.ts:25
GOFF-COLL-020 PASS (accidental — no trackability check exists at all)
GOFF-FALLBACK-003 FAIL (inherited: §16 unimplemented)
...
Two further finding classes MUST be reported when present, because neither has a natural home in a requirement-keyed list:
- Documentation contradicting code. Cite both locations. Where the contradiction is an
option that is declared but never read, report it as
GOFF-CFG-009; otherwise report it as a Minor finding with both citations and no requirement identifier. - Tests that assert non-conformant behaviour. A green suite pinning a defect raises the cost of remediation and indicates the behaviour was deliberate. Name the test alongside the finding it protects.
C.3 Auditing method
The procedure for producing such a report — which files to read, in what order, and how to
decide N/A — is maintained alongside the source rather than here, so it can evolve without a
specification revision.
One rule is normative because it was learned the hard way: source is authoritative over documentation. During the audits that produced this specification, every provider's own README, doc comments or agent instructions were wrong about that provider's behaviour, and in several cases documented a default that no code path ever assigned. A contradiction between a provider's documentation and its code MUST be reported as a finding in its own right.
Appendix D — Known gaps
This appendix is populated in a follow-up revision, once the corresponding issues have been filed against each provider repository. Each row will cite its tracking issue.