Skip to main content

Specification Document for GO Feature Flag OpenFeature Providers

Specification Version1.0
Creation Date03/08/2026
Last Update Date03/08/2026
AuthorsThomas Poignant
Minimum relay proxyv1.55.0
Evaluation enginemodules/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.

info

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:

TierApplies to
CoreEvery server-side provider
RemoteProviders offering remote (OFREP) evaluation
In-processProviders offering local evaluation
WASMIn-process providers that embed the engine through WebAssembly
OptionalMAY-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

SeverityMeaning
CriticalWrong flag values, silent data loss, or unrecoverable provider state
MajorObservable misbehaviour, or a contracted capability that is missing
MinorTelemetry 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) is True, so a boolean silently satisfies an integer resolver. See GOFF-EVAL-004.
  • Java: a JSON number above Integer.MAX_VALUE decodes to Long and currently fails both the integer and the double resolver.
  • .NET: GetInt32() throws FormatException on 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.

IDSevRequirement
GOFF-ENG-001CriticalThe 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-002MajorThe 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-003MinorThe 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

IDSevRequirement
GOFF-META-001MinorThe provider metadata name MUST be exactly GO Feature Flag Provider.
GOFF-META-002MinorThe 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-003MinorAny 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 nameTypeDefaultApplies toSupport
endpointURL string(required)bothREQUIRED
evaluationTypeenumin-processbothREQUIRED
apiKeystringnonebothREQUIRED
timeoutduration10000 msbothREQUIRED
flagChangePollingIntervalduration120000 msin-processREQUIRED
exporterMetadatamapemptybothREQUIRED
dataFlushIntervalduration60000 msbothREQUIRED
maxPendingEventsinteger10000bothREQUIRED
disableDataCollectionbooleanfalsebothREQUIRED
dataCollectorBaseURLURL stringendpointbothRECOMMENDED
evaluationFlagListstring listempty (all)in-processRECOMMENDED
wasmEvaluatorPoolSizeintegerCPU core countWASM †RECOMMENDED
loggerSDK loggerlanguage defaultbothRECOMMENDED

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

IDSevRequirement
GOFF-CFG-001Majorendpoint MUST be required and validated at construction time. An absent or malformed value MUST raise a configuration error before any network activity.
GOFF-CFG-002MajorThe default evaluation mode MUST be in-process.
GOFF-CFG-003MajorEvery option listed in §3.1 that the provider supports MUST use the default value given there.
GOFF-CFG-004MajorThe provider MUST NOT mutate the caller's options object or any collection it contains. Normalisation MUST operate on a copy.
GOFF-CFG-005CriticalThe 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-006MajordataCollectorBaseURL 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-007MajorWhere 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-008MinorevaluationFlagList SHOULD be supported, and when non-empty MUST be transmitted as the flags array of the flag-configuration request.
GOFF-CFG-009MajorAn 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-010MinorThe 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

IDSevRequirement
GOFF-LIFE-001MajorInitialization MUST block until the provider can serve evaluations, or fail.
GOFF-LIFE-002CriticalInitialization 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-003CriticalAny one-shot shutdown guard MUST be reset by re-initialization, so that a subsequent shutdown cannot hang or panic.
GOFF-LIFE-004MajorShutdown 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-005MajorShutdown MUST bound how long it waits for background work.
GOFF-LIFE-006CriticalUntil 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-007MajorWhere 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

IDSevRequirement
GOFF-EVT-001MajorProvider 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-002MajorPROVIDER_CONFIGURATION_CHANGED MUST be emitted when a poll yields a configuration different from the one in use.
GOFF-EVT-003MajorPROVIDER_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-004MajorPROVIDER_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 ETagMUST compare content rather than emit unconditionally.
GOFF-EVT-005MajorThe provider SHOULD emit PROVIDER_STALE after 3 consecutive failed configuration refreshes, and MUST continue serving the last known-good configuration.
GOFF-EVT-006MajorOn recovery from stale, the provider MUST emit an event returning it to ready.
GOFF-EVT-007CriticalAuthentication failure (401, 403) during initialization MUST put the provider in PROVIDER_FATAL. Credentials cannot be repaired by retrying.
GOFF-EVT-008MajorEvery 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

IDSevRequirement
GOFF-EVAL-001MajorThe 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-002CriticalThe 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-003CriticalThe float resolver MUST accept an integral JSON number. JSON does not distinguish 100 from 100.0.
GOFF-EVAL-004CriticalA boolean value MUST NOT satisfy the integer or float resolver. It MUST report TYPE_MISMATCH.
GOFF-EVAL-005MajorWhere 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-006CriticalA 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-007MajorThe 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-008MajorA disabled flag MUST return the caller's default value with reason DISABLED and variant SdkDefault.
GOFF-EVAL-009MajorFlag metadata MUST be passed through with its structure intact. Values MUST NOT be coerced to strings.
GOFF-EVAL-010MajorMetadata 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-011MajorOn 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

IDSevRequirement
GOFF-CTX-001MajorThe targeting key MUST be transmitted under the key targetingKey.
GOFF-CTX-002MajorContext attributes MUST be flattened alongside targetingKey, not nested under a wrapper.
GOFF-CTX-003CriticalA 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-004CriticalIn-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-005Major(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:

FieldWritten byPurpose
exporterMetadataproviderStatic metadata attached to exported evaluation events
flagListcallerRestricts which flags a bulk evaluation returns
currentDateTimecallerOverrides evaluation time, for testing scheduled rollouts
IDSevRequirement
GOFF-CTX-006CriticalThe 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-007MajorexporterMetadata MUST be nested under gofeatureflag.exporterMetadata. Writing the metadata flat under gofeatureflag means the server never reads it.
GOFF-CTX-008MajorIf gofeatureflag is present but not a map, the provider MUST replace it rather than fail.
GOFF-CTX-009MajorThe 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.

IDSevRequirement
GOFF-REM-001MajorSingle-flag evaluation MUST be POST {endpoint}/ofrep/v1/evaluate/flags/{flagKey}.
GOFF-REM-002MajorThe request body MUST be {"context": { ... }} containing targetingKey and the flattened attributes.
GOFF-REM-003MajorThe 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-004MajorThe provider MUST honour 429 responses by respecting Retry-After before issuing further requests.
GOFF-REM-005MajorThe provider MUST NOT implement any other retry behaviour. Polling and the caller's own retry policy are the recovery mechanisms.
GOFF-REM-006MajorThe 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

IDSevRequirement
GOFF-IP-001MajorThe provider MUST fetch configuration with POST {endpoint}/v1/flag/configuration.
GOFF-IP-002MajorThe request body MUST be {"flags": [...]}, an empty array meaning "all flags".
GOFF-IP-003CriticalAny path prefix on endpoint MUST be preserved. Building the URL from an absolute path discards it and silently retargets the request.
GOFF-IP-004MajorThe response flags and evaluationContextEnrichment MUST both be stored.
GOFF-IP-005MajorThe 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

IDSevRequirement
GOFF-IP-006CriticalThe 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-007CriticalA 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-015MajorA 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-008CriticalA 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-009CriticalA 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-010MajorA failed refresh MUST NOT terminate polling. Polling MUST survive any error and continue on schedule.
GOFF-IP-011MinorThe provider SHOULD apply jitter to the polling interval so that a restarted fleet does not poll in lockstep.
GOFF-IP-018MinorA 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-019MajorA 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.
Recommended implementation

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

IDSevRequirement
GOFF-IP-012MajorA flag absent from the local configuration MUST yield FLAG_NOT_FOUND without invoking the engine.
GOFF-IP-013CriticalEvaluation 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-014MajorThe engine input MUST carry flagKey, flag, evalContext, and flagContext containing defaultSdkValue and evaluationContextEnrichment.
GOFF-IP-016CriticalThe 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-017MajorUnknown 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.
Why the flag object is opaque

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)
Read the output before calling free

The 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.

IDSevRequirement
GOFF-WASM-001MajorThe host MUST resolve the exports memory, malloc, free and evaluate, and MUST fail initialization if any is absent.
GOFF-WASM-002MajorIf _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-003CriticalThe 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-004MajorThe 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-005CriticalThe 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-006MajorThe 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-007CriticalThe 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-008CriticalIf 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-012CriticalAfter 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-009MajorWhere 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/AGOFF-WASM-007 is satisfied there without a pool.
GOFF-WASM-010MajorThe engine binary version MUST be pinned to a single, machine-readable value.
GOFF-WASM-011MinorThe 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:

GuardLimit
Input JSON nesting depth128 levels
nikunjy query nesting64 brackets/parentheses
JSONLogic document nesting256 levels
nikunjy [...] lists and JSONLogic operand arrays1,000 items
nikunjy and/or conditions1,000
nikunjy attribute path segments128
IDSevRequirement
GOFF-WASM-013MajorThe 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

IDSevRequirement
GOFF-ERR-001MajorEngine error codes with an SDK equivalent MUST be mapped to it.
GOFF-ERR-002MajorAny unrecognised error code MUST map to GENERAL. It MUST NOT map to null, and MUST NOT raise an unmapped language-level exception.
GOFF-ERR-003MajorerrorDetails MUST be carried through as the error message.
GOFF-ERR-004MajorHTTP failures MUST be distinguishable by status. 401 and 403 MUST be reported distinctly from 404, 400, 429 and 5xx.
GOFF-ERR-005MajorErrors raised during background refresh MUST be logged and MUST NOT propagate to the application.
GOFF-ERR-006MinorData-collector failures MUST be logged. Silently discarding them makes a permanently failing exporter undetectable.

12. Hooks — GOFF-HOOK

Tier: Core

IDSevRequirement
GOFF-HOOK-001MajorThe provider's hooks MUST be observable by the time initialization completes, in the order [EnrichEvaluationContext, DataCollector].
GOFF-HOOK-002MajorRepeated initialization MUST NOT duplicate hooks.
GOFF-HOOK-003MajorThe 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-004MajorThe enrichment hook MUST implement only the before stage, and MUST return a new context rather than mutating the caller's.
GOFF-HOOK-005MajorThe data-collector hook MUST implement the after and error stages.
GOFF-HOOK-006MajorBoth 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": [ ... ]
}
IDSevRequirement
GOFF-COLL-001MajorEvents MUST be posted to POST {dataCollectorBaseURL}/v1/data/collector.
GOFF-COLL-002CriticalThe metadata key MUST be meta. The events key MUST be events.

13.2 Feature event

IDSevRequirement
GOFF-COLL-003Majorkind MUST be feature.
GOFF-COLL-004CriticalThe "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-005MajorcontextKind 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-006MajoruserKey MUST be the targeting key, or the sentinel undefined-targetingKey when absent.
GOFF-COLL-007MajorcreationDate MUST be Unix epoch seconds.
GOFF-COLL-008Majorvariation MUST be the resolved variant, or SdkDefault when none is available.
GOFF-COLL-009Withdrawn. 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-010Minorsource 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 attributecontextKind
boolean trueanonymousUser
boolean falseuser
absentuser
evaluation context absentanonymousUser
any non-boolean valueuser

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

IDSevRequirement
GOFF-COLL-011MajorThe 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-012Minorprovider 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-013MajorexporterMetadata 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

IDSevRequirement
GOFF-COLL-014MajorEvents MUST be flushed on the configured interval, when the buffer reaches maxPendingEvents, and on shutdown.
GOFF-COLL-015MinorThe publisher MUST NOT flush immediately on start. There is nothing to send.
GOFF-COLL-016MajorFlushing MUST be single-flight — concurrent publishes MUST NOT overlap.
GOFF-COLL-017CriticalSingle-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-018MajorA failed batch MUST be re-queued preserving chronological order.
GOFF-COLL-019CriticalThe 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

IDSevRequirement
GOFF-COLL-020MajorA flag whose configuration omits trackEvents MUST be treated as trackable. The engine's own default is true.
GOFF-COLL-021MajorA flag absent from the configuration MUST be treated as trackable, so that a flag added between polls still produces data.
GOFF-COLL-022MajorA flag whose configuration sets trackEvents: false MUST NOT produce feature events.
GOFF-COLL-023MajorIn 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.

IDSevRequirement
GOFF-TRACK-001MajorThe provider MUST implement the SDK's Tracking API where one exists.
GOFF-TRACK-002MajorTracking events MUST be sent in both evaluation modes. The relay proxy does not synthesise custom events.
GOFF-TRACK-003MajorTracking events MUST honour disableDataCollection.
GOFF-TRACK-004Majorkind MUST be tracking, and the details field MUST be trackingEventDetails.
GOFF-TRACK-005MajorThe event MUST carry evaluationContext, and MUST use the same contextKind, userKey and creationDate rules as §13.

15. Authentication — GOFF-AUTH

Tier: Core

IDSevRequirement
GOFF-AUTH-001MajorWhen apiKey is set, the provider MUST send X-API-Key: {apiKey}.
GOFF-AUTH-002MajorWhatever 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-003MajorWhen apiKey is unset or empty, no authentication header MUST be sent.
GOFF-AUTH-004MinorThe provider SHOULD allow arbitrary additional headers, for deployments behind gateways requiring their own authentication.
Why X-API-Key

The 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.

IDSevRequirement
GOFF-FALLBACK-001MajorWhen 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-002MajorThe trigger MUST be evaluated against the raw engine error code, before mapping to the SDK's error enumeration.
GOFF-FALLBACK-003MajorFLAG_CONFIG MUST NOT trigger a fallback. It is a deterministic misconfiguration the relay proxy would reproduce identically.
GOFF-FALLBACK-004MajorThe fallback MUST be attempted on every qualifying occurrence.
GOFF-FALLBACK-005MajorIf 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-006MajorA fallback result MUST NOT emit a feature event. The relay proxy has already recorded it.
GOFF-FALLBACK-007MajorA fallback result MUST carry flag metadata gofeatureflag_evaluated_remotely: true.
GOFF-FALLBACK-008MajorEach fallback MUST be logged at warning level.
GOFF-FALLBACK-009MajorAuthentication 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.

Operational consequence

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.

IDSevRequirement
GOFF-CACHE-001MajorThe cache MUST be an LRU with configurable maximum size and TTL, defaulting to 10000 entries and 60 s.
GOFF-CACHE-002MajorA TTL of -1 MUST mean entries never expire.
GOFF-CACHE-003MajorWhen present, the cache MUST be enabled by default and MUST be possible to disable by configuration.
GOFF-CACHE-004MajorThe cache key MUST combine the flag key and the evaluation context.
GOFF-CACHE-005MajorEvaluation MUST consult the cache before calling the relay proxy, and MUST populate it on a miss.
GOFF-CACHE-006MajorExpired entries MUST NOT be served, and the oldest entry MUST be evicted when the maximum size is reached.
GOFF-CACHE-007MajorCache 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

MethodPathPurpose
POST/ofrep/v1/evaluate/flags/{flagKey}Remote single-flag evaluation
POST/ofrep/v1/evaluate/flagsRemote bulk evaluation
POST/v1/flag/configurationIn-process configuration fetch
POST/v1/data/collectorEvaluation and tracking events

A.2 Superseded

These remain functional but MUST NOT be used by a conformant provider.

MethodPathReplaced 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"}.

FlagValueVariantReasonError code
bool_targeting_matchtrueenabledTARGETING_MATCH
string_key"CC0002"color1STATIC
double_key101.25mediumTARGETING_MATCH
integer_key101mediumTARGETING_MATCH
object_key{"test":"false"}varBTARGETING_MATCH
disabled_boolSDK defaultSdkDefaultDISABLED
DOES_NOT_EXISTSDK defaultERRORFLAG_NOT_FOUND
string_key (as boolean)SDK defaultERRORTYPE_MISMATCH

Notes that catch real bugs:

  • string_key resolves via its default rule with no targeting match, so the reason is STATIC, not TARGETING_MATCH.
  • string_key sets trackEvents: false and therefore MUST NOT produce a feature event (GOFF-COLL-022).
  • A disabled flag returns the caller's default with variant SdkDefault — not defaultSdk, 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:

  1. a header stating the specification version, the provider and version audited, and the declared tier set — every N/A must trace back to it;
  2. a single verdict line;
  3. findings grouped by severity, most severe first, each citing a requirement identifier and a source location;
  4. a per-requirement verdict table covering every identifier in the specification, including passes, since PASS with 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.