From 1ad4edfb2f4a7fbb47ff96162fa6a9cc0c050e4f Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Mon, 16 Mar 2026 16:24:04 -0400 Subject: [PATCH 01/15] feat: implement ADR-0008 SSE event stream support in OpenAPI spec - Bump OpenAPI version from 3.1.0 to 3.2.0 for text/event-stream support - Bump OFREP version from 0.2.0 to 0.3.0 - Add flagConfigEtag and flagConfigLastModified query parameters to both eval endpoints - Add eventStreams field to bulkEvaluationSuccess and serverEvaluationSuccess responses - Add eventStream schema with mutually exclusive url/endpoint fields - Add eventStreamEndpoint schema for structured origin + requestUri - Add sseEvent and sseEventData schemas for event stream payloads - Add webhook documenting text/event-stream content type (OAS 3.2.0) - Disable oas3-schema spectral rule until 3.2.0 support lands (stoplightio/spectral#2910) Signed-off-by: Jonathan Norris --- .spectral.yaml | 3 + service/openapi.yaml | 267 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 268 insertions(+), 2 deletions(-) diff --git a/.spectral.yaml b/.spectral.yaml index 77292bf..a476373 100644 --- a/.spectral.yaml +++ b/.spectral.yaml @@ -2,3 +2,6 @@ extends: - spectral:oas rules: oas3-valid-media-example: off + # Disabled until spectral supports OpenAPI 3.2.0 + # See: https://github.com/stoplightio/spectral/issues/2910 + oas3-schema: off diff --git a/service/openapi.yaml b/service/openapi.yaml index f04804e..a603d90 100644 --- a/service/openapi.yaml +++ b/service/openapi.yaml @@ -1,8 +1,8 @@ -openapi: 3.1.0 +openapi: 3.2.0 servers: - url: / info: - version: 0.2.0 + version: 0.3.0 title: OpenFeature Remote Evaluation Protocol (OFREP) description: | --- @@ -54,6 +54,40 @@ paths: schema: type: string example: discount-banner + - in: query + name: flagConfigEtag + description: | + Optional ETag metadata provided by an event stream for change-triggered + re-fetches (see ADR-0008). This is not a standard HTTP conditional request + header; it is metadata for server-side cache validation and freshness + checks. It should only be included when the request is directly triggered + by a received change notification event. + schema: + type: string + required: false + example: '"550e8400-e29b-41d4-a716-446655440000"' + - in: query + name: flagConfigLastModified + description: | + Optional last-modified metadata provided by an event stream for + change-triggered re-fetches (see ADR-0008). Supports Unix timestamp in + seconds (recommended) or a date string (ISO 8601 / HTTP-date), and is + transported as query metadata rather than `If-Modified-Since`. It should + only be included when the request is directly triggered by a received + change notification event. + schema: + oneOf: + - type: integer + minimum: 0 + - type: string + required: false + examples: + epochSeconds: + value: 1771622898 + isoDate: + value: "2026-02-20T21:28:18Z" + httpDate: + value: "Thu, 20 Feb 2026 21:28:18 GMT" requestBody: required: true description: Evaluation request containing the context for flag evaluation @@ -79,6 +113,10 @@ paths: value: true reason: TARGETING_MATCH variant: enabled + eventStreams: + - type: sse + url: https://sse.example.com/event-stream?channels=env_abc123_v1 + inactivityDelaySec: 120 "400": description: Bad evaluation request. The request is malformed or contains invalid context. content: @@ -147,6 +185,40 @@ paths: type: string required: false example: '"abc123xyz"' + - in: query + name: flagConfigEtag + description: | + Optional ETag metadata provided by an event stream for change-triggered + re-fetches (see ADR-0008). This is not a standard HTTP conditional request + header; it is metadata for server-side cache validation and freshness + checks. It should only be included when the request is directly triggered + by a received change notification event. + schema: + type: string + required: false + example: '"550e8400-e29b-41d4-a716-446655440000"' + - in: query + name: flagConfigLastModified + description: | + Optional last-modified metadata provided by an event stream for + change-triggered re-fetches (see ADR-0008). Supports Unix timestamp in + seconds (recommended) or a date string (ISO 8601 / HTTP-date), and is + transported as query metadata rather than `If-Modified-Since`. It should + only be included when the request is directly triggered by a received + change notification event. + schema: + oneOf: + - type: integer + minimum: 0 + - type: string + required: false + examples: + epochSeconds: + value: 1771622898 + isoDate: + value: "2026-02-20T21:28:18Z" + httpDate: + value: "Thu, 20 Feb 2026 21:28:18 GMT" requestBody: required: true content: @@ -188,6 +260,10 @@ paths: - key: non-existent-flag errorCode: FLAG_NOT_FOUND errorDetails: "Flag 'non-existent-flag' was not found" + eventStreams: + - type: sse + url: https://sse.example.com/event-stream?channels=env_abc123_v1 + inactivityDelaySec: 120 metadata: version: v12 "304": @@ -228,6 +304,29 @@ paths: $ref: "#/components/schemas/generalErrorResponse" example: errorDetails: "An internal server error occurred while processing the request" +webhooks: + flagConfigChanged: + post: + summary: SSE Flag Configuration Change Notification + description: | + Describes the Server-Sent Events (SSE) stream that providers receive from + the URLs specified in `eventStreams`. Events signal that the underlying flag + configuration has changed and the provider should re-fetch evaluations. + + This webhook documents the event format only; the actual SSE endpoint URLs + are opaque and vendor-provided via the `eventStreams` field in evaluation responses. + tags: [OFREP Core] + operationId: flagConfigChangedEvent + requestBody: + description: SSE event stream carrying flag configuration change notifications + content: + text/event-stream: + itemSchema: + $ref: "#/components/schemas/sseEvent" + responses: + "200": + description: SSE connection established successfully + components: securitySchemes: BearerAuth: @@ -276,6 +375,16 @@ components: $ref: "#/components/schemas/metadata" description: | Arbitrary metadata for the flag set, useful for telemetry and documentary purposes. + eventStreams: + type: array + description: | + Optional array of real-time change notification connections. When present, + the provider should connect to any entries with a known type and re-fetch + flag evaluations when notified of changes. If not present, the provider + should continue using polling for change detection. Entries with unknown + types must be ignored for forward compatibility. + items: + $ref: "#/components/schemas/eventStream" bulkEvaluationFailure: description: | Failure response for bulk evaluation. Returned when the entire bulk evaluation request @@ -308,6 +417,160 @@ components: serverEvaluationSuccess: allOf: - $ref: "#/components/schemas/evaluationSuccess" + - type: object + properties: + eventStreams: + type: array + description: | + Optional array of real-time change notification connections. When present, + the provider should connect to any entries with a known type and re-fetch + flag evaluations when notified of changes. If not present, the provider + should continue using polling for change detection. Entries with unknown + types must be ignored for forward compatibility. + items: + $ref: "#/components/schemas/eventStream" + eventStream: + description: | + A real-time change notification connection endpoint. The `type` field + identifies the push mechanism; currently only `sse` is defined. Providers + must ignore entries with unknown types for forward compatibility. + Exactly one of `url` or `endpoint` must be provided. + type: object + required: + - type + oneOf: + - required: + - url + not: + required: + - endpoint + - required: + - endpoint + not: + required: + - url + properties: + type: + type: string + description: | + The connection type identifying the push mechanism to use. + Currently only `sse` is defined. Providers must ignore entries + with unknown types for forward compatibility. + example: "sse" + url: + type: string + format: uri + description: | + The endpoint URL the client should connect to for real-time + flag change notifications. This is the default representation and + is opaque to the provider. The URL may include authentication tokens, + channel identifiers, or other query parameters as needed by the + vendor's infrastructure. Implementations should treat this value as + sensitive and should not log or persist the full URL including its + query string. + example: "https://sse.example.com/event-stream?channels=env_abc123_v1" + endpoint: + $ref: "#/components/schemas/eventStreamEndpoint" + inactivityDelaySec: + type: integer + minimum: 1 + description: | + Number of seconds of client inactivity (e.g., browser tab hidden, + mobile app backgrounded) after which the connection should be closed + to conserve resources. The client must reconnect and perform a full + unconditional re-fetch when activity resumes. When determining the + effective inactivity timeout, providers should use a client-side + override if configured; otherwise use this value when present; + otherwise default to 120 seconds. + example: 120 + eventStreamEndpoint: + type: object + required: + - origin + - requestUri + description: | + Structured endpoint components for deployments that need to override + the origin cleanly while preserving the request target. When present, + providers construct the connection URL as `origin + requestUri`. + properties: + origin: + type: string + format: uri + description: | + The scheme + host + optional port portion of the endpoint URL. + example: "https://sse.example.com" + requestUri: + type: string + description: | + The path + query portion of the endpoint URL. + example: "/event-stream?channels=env_abc123_v1" + sseEvent: + description: | + Schema for a single Server-Sent Event in the flag configuration change + notification stream. The `data` field contains a JSON-encoded payload + that providers must parse to determine the event type and any metadata. + type: object + required: + - data + properties: + data: + type: string + description: | + JSON-encoded event payload. Providers must parse this string as JSON + and inspect the `type` field to determine behavior. + contentMediaType: application/json + contentSchema: + $ref: "#/components/schemas/sseEventData" + event: + type: string + description: | + The SSE event type. Always `message` for OFREP events. Providers + must inspect `data.type` rather than this field for event routing. + example: "message" + id: + type: string + description: | + Event identifier used by SSE clients for resume semantics via + `Last-Event-ID`. + example: "evt-1234" + retry: + type: integer + minimum: 0 + description: | + Reconnection time in milliseconds suggested by the server. + sseEventData: + description: | + JSON payload inside the event `data` field. The `type` field determines + the event semantics. Providers must handle `refetchEvaluation` and + ignore unknown values for forward compatibility. + type: object + required: + - type + properties: + type: + type: string + description: | + The OFREP event type. Currently only `refetchEvaluation` is defined. + Providers must ignore unknown values for forward compatibility. + example: "refetchEvaluation" + etag: + type: string + description: | + Latest flag configuration cache validation token. If present, + providers should include it as the `flagConfigEtag` query parameter + on the re-fetch request. + example: '"abc123"' + lastModified: + description: | + Latest flag configuration timestamp. Supports Unix timestamp in + seconds (recommended) or a date string (ISO 8601 or HTTP-date). + If present, providers should include it as the `flagConfigLastModified` + query parameter on the re-fetch request. + oneOf: + - type: integer + minimum: 0 + - type: string + example: 1771622898 evaluationSuccess: description: | Successful feature flag evaluation response. The value property is present From 1086ab1e02382526de94d997cec2e9ced1234a5f Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Mon, 16 Mar 2026 16:27:31 -0400 Subject: [PATCH 02/15] refactor: move query params to components/parameters Signed-off-by: Jonathan Norris --- service/openapi.yaml | 109 ++++++++++++++++--------------------------- 1 file changed, 41 insertions(+), 68 deletions(-) diff --git a/service/openapi.yaml b/service/openapi.yaml index a603d90..e06ead0 100644 --- a/service/openapi.yaml +++ b/service/openapi.yaml @@ -54,40 +54,8 @@ paths: schema: type: string example: discount-banner - - in: query - name: flagConfigEtag - description: | - Optional ETag metadata provided by an event stream for change-triggered - re-fetches (see ADR-0008). This is not a standard HTTP conditional request - header; it is metadata for server-side cache validation and freshness - checks. It should only be included when the request is directly triggered - by a received change notification event. - schema: - type: string - required: false - example: '"550e8400-e29b-41d4-a716-446655440000"' - - in: query - name: flagConfigLastModified - description: | - Optional last-modified metadata provided by an event stream for - change-triggered re-fetches (see ADR-0008). Supports Unix timestamp in - seconds (recommended) or a date string (ISO 8601 / HTTP-date), and is - transported as query metadata rather than `If-Modified-Since`. It should - only be included when the request is directly triggered by a received - change notification event. - schema: - oneOf: - - type: integer - minimum: 0 - - type: string - required: false - examples: - epochSeconds: - value: 1771622898 - isoDate: - value: "2026-02-20T21:28:18Z" - httpDate: - value: "Thu, 20 Feb 2026 21:28:18 GMT" + - $ref: "#/components/parameters/flagConfigEtag" + - $ref: "#/components/parameters/flagConfigLastModified" requestBody: required: true description: Evaluation request containing the context for flag evaluation @@ -185,40 +153,8 @@ paths: type: string required: false example: '"abc123xyz"' - - in: query - name: flagConfigEtag - description: | - Optional ETag metadata provided by an event stream for change-triggered - re-fetches (see ADR-0008). This is not a standard HTTP conditional request - header; it is metadata for server-side cache validation and freshness - checks. It should only be included when the request is directly triggered - by a received change notification event. - schema: - type: string - required: false - example: '"550e8400-e29b-41d4-a716-446655440000"' - - in: query - name: flagConfigLastModified - description: | - Optional last-modified metadata provided by an event stream for - change-triggered re-fetches (see ADR-0008). Supports Unix timestamp in - seconds (recommended) or a date string (ISO 8601 / HTTP-date), and is - transported as query metadata rather than `If-Modified-Since`. It should - only be included when the request is directly triggered by a received - change notification event. - schema: - oneOf: - - type: integer - minimum: 0 - - type: string - required: false - examples: - epochSeconds: - value: 1771622898 - isoDate: - value: "2026-02-20T21:28:18Z" - httpDate: - value: "Thu, 20 Feb 2026 21:28:18 GMT" + - $ref: "#/components/parameters/flagConfigEtag" + - $ref: "#/components/parameters/flagConfigLastModified" requestBody: required: true content: @@ -344,6 +280,43 @@ components: type: apiKey in: header name: X-API-Key + parameters: + flagConfigEtag: + in: query + name: flagConfigEtag + description: | + Optional ETag metadata provided by an event stream for change-triggered + re-fetches (see ADR-0008). This is not a standard HTTP conditional request + header; it is metadata for server-side cache validation and freshness + checks. It should only be included when the request is directly triggered + by a received change notification event. + schema: + type: string + required: false + example: '"550e8400-e29b-41d4-a716-446655440000"' + flagConfigLastModified: + in: query + name: flagConfigLastModified + description: | + Optional last-modified metadata provided by an event stream for + change-triggered re-fetches (see ADR-0008). Supports Unix timestamp in + seconds (recommended) or a date string (ISO 8601 / HTTP-date), and is + transported as query metadata rather than `If-Modified-Since`. It should + only be included when the request is directly triggered by a received + change notification event. + schema: + oneOf: + - type: integer + minimum: 0 + - type: string + required: false + examples: + epochSeconds: + value: 1771622898 + isoDate: + value: "2026-02-20T21:28:18Z" + httpDate: + value: "Thu, 20 Feb 2026 21:28:18 GMT" schemas: bulkEvaluationRequest: description: | From ba0a45baf8b6ccd502f678dadc02c8ba560a91b2 Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Mon, 16 Mar 2026 16:30:33 -0400 Subject: [PATCH 03/15] refactor: move webhooks to end of file, add Event Streams tag Signed-off-by: Jonathan Norris --- service/openapi.yaml | 47 ++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/service/openapi.yaml b/service/openapi.yaml index e06ead0..f85a7cd 100644 --- a/service/openapi.yaml +++ b/service/openapi.yaml @@ -28,6 +28,9 @@ tags: description: | **Required**: Core APIs to implement to support OFREP. *This is the minimum set of APIs required for a flag management system to be OFREP compatible.* + - name: Event Streams + description: | + **Optional**: Real-time change notification mechanisms for flag configuration updates (see ADR-0008). paths: /ofrep/v1/evaluate/flags/{key}: @@ -240,29 +243,6 @@ paths: $ref: "#/components/schemas/generalErrorResponse" example: errorDetails: "An internal server error occurred while processing the request" -webhooks: - flagConfigChanged: - post: - summary: SSE Flag Configuration Change Notification - description: | - Describes the Server-Sent Events (SSE) stream that providers receive from - the URLs specified in `eventStreams`. Events signal that the underlying flag - configuration has changed and the provider should re-fetch evaluations. - - This webhook documents the event format only; the actual SSE endpoint URLs - are opaque and vendor-provided via the `eventStreams` field in evaluation responses. - tags: [OFREP Core] - operationId: flagConfigChangedEvent - requestBody: - description: SSE event stream carrying flag configuration change notifications - content: - text/event-stream: - itemSchema: - $ref: "#/components/schemas/sseEvent" - responses: - "200": - description: SSE connection established successfully - components: securitySchemes: BearerAuth: @@ -734,3 +714,24 @@ components: flagMetadataDescription: description: | Arbitrary metadata for the flag, useful for telemetry and documentary purposes. +webhooks: + flagConfigChanged: + post: + summary: SSE Flag Configuration Change Notification + description: | + Describes the Server-Sent Events (SSE) stream that providers receive from + the URLs specified in `eventStreams`. Events signal that the underlying flag + configuration has changed and the provider should re-fetch evaluations. + + This webhook documents the event format only; the actual SSE endpoint URLs + are opaque and vendor-provided via the `eventStreams` field in evaluation responses. + tags: [Event Streams] + requestBody: + description: SSE event stream carrying flag configuration change notifications + content: + text/event-stream: + itemSchema: + $ref: "#/components/schemas/sseEvent" + responses: + "200": + description: SSE connection established successfully From f8a032852a8e62f22790d2f5cb80518f266487e3 Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Wed, 18 Mar 2026 21:52:05 -0400 Subject: [PATCH 04/15] fix: use normative 'must not' for URL sensitivity per ADR-0008 Signed-off-by: Jonathan Norris --- service/openapi.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/service/openapi.yaml b/service/openapi.yaml index f85a7cd..c5f720a 100644 --- a/service/openapi.yaml +++ b/service/openapi.yaml @@ -418,8 +418,8 @@ components: flag change notifications. This is the default representation and is opaque to the provider. The URL may include authentication tokens, channel identifiers, or other query parameters as needed by the - vendor's infrastructure. Implementations should treat this value as - sensitive and should not log or persist the full URL including its + vendor's infrastructure. Implementations must treat this value as + sensitive and must not log or persist the full URL including its query string. example: "https://sse.example.com/event-stream?channels=env_abc123_v1" endpoint: From 3b3dbe49cbeccb852c651199ac3625aa0d848617 Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Wed, 18 Mar 2026 22:00:53 -0400 Subject: [PATCH 05/15] fix: require leading slash on requestUri Signed-off-by: Jonathan Norris --- service/openapi.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/service/openapi.yaml b/service/openapi.yaml index c5f720a..bf74a2a 100644 --- a/service/openapi.yaml +++ b/service/openapi.yaml @@ -454,8 +454,9 @@ components: example: "https://sse.example.com" requestUri: type: string + pattern: "^/" description: | - The path + query portion of the endpoint URL. + The path + query portion of the endpoint URL. Must start with `/`. example: "/event-stream?channels=env_abc123_v1" sseEvent: description: | From e76cddc7c6a69f581c1b54bde3808d985352f087 Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Mon, 30 Mar 2026 14:32:47 -0400 Subject: [PATCH 06/15] refactor: scope eventStreams to bulk evaluation endpoint only Signed-off-by: Jonathan Norris --- .../0008-sse-for-bulk-evaluation-changes.md | 14 +++++++------- service/openapi.yaml | 18 ------------------ 2 files changed, 7 insertions(+), 25 deletions(-) diff --git a/service/adrs/0008-sse-for-bulk-evaluation-changes.md b/service/adrs/0008-sse-for-bulk-evaluation-changes.md index 112d62a..9f8fe27 100644 --- a/service/adrs/0008-sse-for-bulk-evaluation-changes.md +++ b/service/adrs/0008-sse-for-bulk-evaluation-changes.md @@ -1,4 +1,4 @@ -# 8. Server-Sent Events (SSE) for bulk evaluation changes +# 8. Server-Sent Events (SSE) for bulk evaluation changes (client-side providers) Date: 2026-02-20 @@ -10,7 +10,7 @@ Proposed OFREP currently relies exclusively on polling for flag change detection. As described in [ADR-0005](0005-polling-for-bulk-evaluation-changes.md), polling was chosen initially for simplicity, with the explicit expectation that additional change detection mechanisms would be added later. -This ADR defines SSE as a real-time change notification mechanism for OFREP. The primary use case is static-context providers (web and mobile) that use bulk evaluation caching, but SSE is also applicable to server-side providers using individual flag evaluations. A standalone endpoint for providers doing in-process local evaluation (outside of OFREP) is deferred to a follow-up ADR. +This ADR defines SSE as a real-time change notification mechanism for OFREP, scoped to client-side providers that use bulk evaluation caching. SSE support for server-side providers using individual flag evaluations and for providers doing in-process local evaluation (outside of OFREP) is deferred to a follow-up ADR. Polling has known limitations: - There is no way to implement real-time flag updates @@ -28,13 +28,13 @@ Server-Sent Events (SSE) is a W3C standard that fits this use case well: ## Decision -Add an optional `eventStreams` array to the bulk evaluation response (`POST /ofrep/v1/evaluate/flags`) and the single flag evaluation response (`POST /ofrep/v1/evaluate/flags/{key}`). When present, it provides connection endpoints that the provider connects to for real-time flag change notifications. +Add an optional `eventStreams` array to the bulk evaluation response (`POST /ofrep/v1/evaluate/flags`). When present, it provides connection endpoints that the provider connects to for real-time flag change notifications. SSE is used as a **notification-only** mechanism -- events signal the provider to re-fetch evaluations via the existing endpoints, rather than streaming full evaluation payloads. This keeps the SSE message format simple, reuses existing infrastructure, and avoids duplicating evaluation logic. ### Response Schema -Add an optional `eventStreams` field to `bulkEvaluationSuccess` and `serverEvaluationSuccess`: +Add an optional `eventStreams` field to `bulkEvaluationSuccess`: ```json { @@ -156,7 +156,7 @@ Provider implementation guidelines: ### OpenAPI Schema Additions ```yaml -# Add to /ofrep/v1/evaluate/flags and /ofrep/v1/evaluate/flags/{key} POST parameters: +# Add to /ofrep/v1/evaluate/flags POST parameters: - in: query name: flagConfigEtag description: | @@ -191,7 +191,7 @@ Provider implementation guidelines: httpDate: value: "Thu, 20 Feb 2026 21:28:18 GMT" -# Add to bulkEvaluationSuccess.properties and serverEvaluationSuccess.properties: +# Add to bulkEvaluationSuccess.properties: eventStreams: type: array description: | @@ -322,5 +322,5 @@ eventStream: - `polling`: Ignore `eventStreams` and rely solely on polling. - `none`: Perform no background refresh; rely solely on explicit `onContextChange` calls. - **Existing SSE libraries**: The LaunchDarkly open-source SSE client libraries ([Java/Android](https://github.com/launchdarkly/okhttp-eventsource), [.NET](https://github.com/launchdarkly/dotnet-eventsource), [JavaScript](https://github.com/launchdarkly/js-eventsource), [Python](https://github.com/launchdarkly/python-eventsource), [Swift/iOS](https://github.com/launchdarkly/swift-eventsource)) are well-maintained and could be used by OFREP provider implementations. Browser environments can use the native `EventSource` API. -- **Provider guideline updates**: The [static context provider guideline](../../guideline/static-context-provider.md) would need a new section describing SSE connection management alongside the existing polling section. Server-side provider guidelines should also be updated to document SSE usage with single-flag evaluations. +- **Provider guideline updates**: The [static context provider guideline](../../guideline/static-context-provider.md) would need a new section describing SSE connection management alongside the existing polling section. - **Standalone endpoint for local evaluation**: Providers doing in-process local evaluation (outside of OFREP) have no evaluation response to carry `eventStreams`. A standalone endpoint such as `GET /ofrep/v1/eventStreams` that returns just the event stream connection details is deferred to a follow-up ADR. diff --git a/service/openapi.yaml b/service/openapi.yaml index bf74a2a..e2a94e8 100644 --- a/service/openapi.yaml +++ b/service/openapi.yaml @@ -57,8 +57,6 @@ paths: schema: type: string example: discount-banner - - $ref: "#/components/parameters/flagConfigEtag" - - $ref: "#/components/parameters/flagConfigLastModified" requestBody: required: true description: Evaluation request containing the context for flag evaluation @@ -84,10 +82,6 @@ paths: value: true reason: TARGETING_MATCH variant: enabled - eventStreams: - - type: sse - url: https://sse.example.com/event-stream?channels=env_abc123_v1 - inactivityDelaySec: 120 "400": description: Bad evaluation request. The request is malformed or contains invalid context. content: @@ -370,18 +364,6 @@ components: serverEvaluationSuccess: allOf: - $ref: "#/components/schemas/evaluationSuccess" - - type: object - properties: - eventStreams: - type: array - description: | - Optional array of real-time change notification connections. When present, - the provider should connect to any entries with a known type and re-fetch - flag evaluations when notified of changes. If not present, the provider - should continue using polling for change detection. Entries with unknown - types must be ignored for forward compatibility. - items: - $ref: "#/components/schemas/eventStream" eventStream: description: | A real-time change notification connection endpoint. The `type` field From 6a33d50cc2010a337de76c0a7bb46ee3f1bbe3d4 Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Mon, 30 Mar 2026 14:47:48 -0400 Subject: [PATCH 07/15] docs: remove parentheses from ADR title Signed-off-by: Jonathan Norris --- service/adrs/0008-sse-for-bulk-evaluation-changes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/adrs/0008-sse-for-bulk-evaluation-changes.md b/service/adrs/0008-sse-for-bulk-evaluation-changes.md index 9f8fe27..0bcfdb7 100644 --- a/service/adrs/0008-sse-for-bulk-evaluation-changes.md +++ b/service/adrs/0008-sse-for-bulk-evaluation-changes.md @@ -1,4 +1,4 @@ -# 8. Server-Sent Events (SSE) for bulk evaluation changes (client-side providers) +# 8. Server-Sent Events (SSE) for bulk evaluation changes — client-side providers Date: 2026-02-20 From a77d4d8047562be3ad3f3398fcd22b8221c1a960 Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Mon, 30 Mar 2026 15:18:22 -0400 Subject: [PATCH 08/15] fix: correct weekday in HTTP-date example Signed-off-by: Jonathan Norris --- service/adrs/0008-sse-for-bulk-evaluation-changes.md | 2 +- service/openapi.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/service/adrs/0008-sse-for-bulk-evaluation-changes.md b/service/adrs/0008-sse-for-bulk-evaluation-changes.md index 0bcfdb7..8be874f 100644 --- a/service/adrs/0008-sse-for-bulk-evaluation-changes.md +++ b/service/adrs/0008-sse-for-bulk-evaluation-changes.md @@ -189,7 +189,7 @@ Provider implementation guidelines: isoDate: value: "2026-02-20T21:28:18Z" httpDate: - value: "Thu, 20 Feb 2026 21:28:18 GMT" + value: "Fri, 20 Feb 2026 21:28:18 GMT" # Add to bulkEvaluationSuccess.properties: eventStreams: diff --git a/service/openapi.yaml b/service/openapi.yaml index e2a94e8..716db91 100644 --- a/service/openapi.yaml +++ b/service/openapi.yaml @@ -290,7 +290,7 @@ components: isoDate: value: "2026-02-20T21:28:18Z" httpDate: - value: "Thu, 20 Feb 2026 21:28:18 GMT" + value: "Fri, 20 Feb 2026 21:28:18 GMT" schemas: bulkEvaluationRequest: description: | From d34318064b933100992eb0b6af37203cf44a5549 Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Mon, 30 Mar 2026 15:20:35 -0400 Subject: [PATCH 09/15] fix: remove inner double quotes from ETag examples Signed-off-by: Jonathan Norris --- service/openapi.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/service/openapi.yaml b/service/openapi.yaml index 716db91..24b267b 100644 --- a/service/openapi.yaml +++ b/service/openapi.yaml @@ -149,7 +149,7 @@ paths: schema: type: string required: false - example: '"abc123xyz"' + example: abc123xyz - $ref: "#/components/parameters/flagConfigEtag" - $ref: "#/components/parameters/flagConfigLastModified" requestBody: @@ -175,7 +175,7 @@ paths: Entity tag (ETag) representing the current state of all flags. Clients should include this value in subsequent requests using the `If-None-Match` header for cache validation. - example: '"abc123xyz"' + example: abc123xyz content: application/json: schema: @@ -267,7 +267,7 @@ components: schema: type: string required: false - example: '"550e8400-e29b-41d4-a716-446655440000"' + example: 550e8400-e29b-41d4-a716-446655440000 flagConfigLastModified: in: query name: flagConfigLastModified @@ -495,7 +495,7 @@ components: Latest flag configuration cache validation token. If present, providers should include it as the `flagConfigEtag` query parameter on the re-fetch request. - example: '"abc123"' + example: abc123 lastModified: description: | Latest flag configuration timestamp. Supports Unix timestamp in From 3da9c7c1e038d9a7c0023e64d9e7ede4748b487c Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Tue, 31 Mar 2026 15:37:48 -0400 Subject: [PATCH 10/15] Update service/openapi.yaml Co-authored-by: Todd Baert Signed-off-by: Jonathan Norris Signed-off-by: Todd Baert --- service/openapi.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/service/openapi.yaml b/service/openapi.yaml index 24b267b..5f51207 100644 --- a/service/openapi.yaml +++ b/service/openapi.yaml @@ -409,6 +409,7 @@ components: inactivityDelaySec: type: integer minimum: 1 + default: 120 description: | Number of seconds of client inactivity (e.g., browser tab hidden, mobile app backgrounded) after which the connection should be closed From 519503cdd1149f26c978e21b2d83374145829add Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Fri, 10 Apr 2026 15:59:55 +0200 Subject: [PATCH 11/15] refactor: make endpoint.origin optional, default to OFREP base URL Signed-off-by: Jonathan Norris --- service/adrs/0008-sse-for-bulk-evaluation-changes.md | 10 ++++++---- service/openapi.yaml | 6 ++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/service/adrs/0008-sse-for-bulk-evaluation-changes.md b/service/adrs/0008-sse-for-bulk-evaluation-changes.md index 8be874f..a4ac8ab 100644 --- a/service/adrs/0008-sse-for-bulk-evaluation-changes.md +++ b/service/adrs/0008-sse-for-bulk-evaluation-changes.md @@ -62,10 +62,10 @@ Add an optional `eventStreams` field to `bulkEvaluationSuccess`: Each event stream object has: - `type` (string, required): The connection type. Currently `"sse"` is the only defined value. Providers must ignore entries with unknown types for forward compatibility, allowing new push mechanisms to be added without breaking existing clients. - `url` (string, optional): The endpoint URL. This is the default representation and is opaque to the provider. It may include authentication tokens, channel identifiers, or other vendor-specific query parameters. Implementations must treat this URL as sensitive -- it may contain auth tokens or channel credentials -- and must not log or persist the full URL including query string. -- `endpoint` (object, optional): Structured endpoint components for deployments that need to override the origin cleanly (for example, via a proxy) while preserving the request target. If present, it has `origin` and `requestUri` fields. +- `endpoint` (object, optional): Structured endpoint components for deployments that need to override the origin cleanly (for example, via a proxy) while preserving the request target. It has a required `requestUri` field and an optional `origin` field. If `origin` is absent, providers should use their configured OFREP base URL origin. - `inactivityDelaySec` (integer, optional): Seconds of client inactivity (e.g., browser tab hidden, mobile app backgrounded) after which the connection should be closed. The client must reconnect and perform a full unconditional re-fetch when activity resumes. Minimum value is `1`. When determining the effective inactivity timeout, providers should use a client-side override if configured; otherwise use this value when present; otherwise default to `120` seconds. -Exactly one of `url` or `endpoint` must be provided. Providers should use `url` as-is when present. When `endpoint` is present, providers should construct the connection URL as `origin + requestUri`. +Exactly one of `url` or `endpoint` must be provided. Providers should use `url` as-is when present. When `endpoint` is present, providers should construct the connection URL as `origin + requestUri`, where `origin` defaults to the provider's configured OFREP base URL if not specified. The `eventStreams` field is an array to support vendors whose infrastructure may require connections to multiple channels or endpoints (e.g., a global channel for environment-wide changes and a user-specific channel for targeted updates). Many SSE providers support multiple channels on a single URL, so the array will typically contain a single entry. @@ -243,18 +243,20 @@ eventStream: endpoint: type: object required: - - origin - requestUri description: | Structured endpoint components for deployments that need to override the origin cleanly while preserving the request target. When present, - providers construct the connection URL as `origin + requestUri`. + providers construct the connection URL as `origin + requestUri`. If + `origin` is absent, providers should use their configured OFREP base + URL origin. properties: origin: type: string format: uri description: | The scheme + host + optional port portion of the endpoint URL. + If absent, providers should use their configured OFREP base URL origin. example: "https://sse.example.com" requestUri: type: string diff --git a/service/openapi.yaml b/service/openapi.yaml index 5f51207..55fbaf2 100644 --- a/service/openapi.yaml +++ b/service/openapi.yaml @@ -422,18 +422,20 @@ components: eventStreamEndpoint: type: object required: - - origin - requestUri description: | Structured endpoint components for deployments that need to override the origin cleanly while preserving the request target. When present, - providers construct the connection URL as `origin + requestUri`. + providers construct the connection URL as `origin + requestUri`. If + `origin` is absent, providers should use their configured OFREP base + URL origin. properties: origin: type: string format: uri description: | The scheme + host + optional port portion of the endpoint URL. + If absent, providers should use their configured OFREP base URL origin. example: "https://sse.example.com" requestUri: type: string From 6429965154825172724f644f112ffa3e1f41d839 Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Fri, 10 Apr 2026 16:06:28 +0200 Subject: [PATCH 12/15] docs: use static-context terminology per reviewer feedback Signed-off-by: Jonathan Norris --- service/adrs/0008-sse-for-bulk-evaluation-changes.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/service/adrs/0008-sse-for-bulk-evaluation-changes.md b/service/adrs/0008-sse-for-bulk-evaluation-changes.md index a4ac8ab..c142e81 100644 --- a/service/adrs/0008-sse-for-bulk-evaluation-changes.md +++ b/service/adrs/0008-sse-for-bulk-evaluation-changes.md @@ -1,4 +1,4 @@ -# 8. Server-Sent Events (SSE) for bulk evaluation changes — client-side providers +# 8. Server-Sent Events (SSE) for bulk evaluation changes — static-context providers Date: 2026-02-20 @@ -10,7 +10,7 @@ Proposed OFREP currently relies exclusively on polling for flag change detection. As described in [ADR-0005](0005-polling-for-bulk-evaluation-changes.md), polling was chosen initially for simplicity, with the explicit expectation that additional change detection mechanisms would be added later. -This ADR defines SSE as a real-time change notification mechanism for OFREP, scoped to client-side providers that use bulk evaluation caching. SSE support for server-side providers using individual flag evaluations and for providers doing in-process local evaluation (outside of OFREP) is deferred to a follow-up ADR. +This ADR defines SSE as a real-time change notification mechanism for OFREP, scoped to static-context providers that use bulk evaluation caching. SSE support for dynamic-context providers using individual flag evaluations and for providers doing in-process local evaluation (outside of OFREP) is deferred to a follow-up ADR. Polling has known limitations: - There is no way to implement real-time flag updates From 50562e44ad7478ca419712b6b94a43584d8f2573 Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Sat, 11 Apr 2026 13:38:19 +0200 Subject: [PATCH 13/15] refactor: move SSE event format from webhooks to components/pathItems Signed-off-by: Jonathan Norris --- service/openapi.yaml | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/service/openapi.yaml b/service/openapi.yaml index 55fbaf2..7533413 100644 --- a/service/openapi.yaml +++ b/service/openapi.yaml @@ -254,6 +254,27 @@ components: type: apiKey in: header name: X-API-Key + pathItems: + eventStreamConnection: + get: + tags: [Event Streams] + summary: SSE Flag Configuration Change Notification + description: | + Describes the Server-Sent Events (SSE) stream that providers receive from + the URLs specified in `eventStreams`. Events signal that the underlying flag + configuration has changed and the provider should re-fetch evaluations. + + This path item documents the event format only; the actual SSE endpoint URLs + are opaque and vendor-provided via the `eventStreams` field in evaluation + responses. Providers should connect to the URL returned in `eventStreams` + rather than constructing a URL from this path item. + responses: + "200": + description: SSE event stream connection established successfully + content: + text/event-stream: + itemSchema: + $ref: "#/components/schemas/sseEvent" parameters: flagConfigEtag: in: query @@ -700,24 +721,4 @@ components: flagMetadataDescription: description: | Arbitrary metadata for the flag, useful for telemetry and documentary purposes. -webhooks: - flagConfigChanged: - post: - summary: SSE Flag Configuration Change Notification - description: | - Describes the Server-Sent Events (SSE) stream that providers receive from - the URLs specified in `eventStreams`. Events signal that the underlying flag - configuration has changed and the provider should re-fetch evaluations. - This webhook documents the event format only; the actual SSE endpoint URLs - are opaque and vendor-provided via the `eventStreams` field in evaluation responses. - tags: [Event Streams] - requestBody: - description: SSE event stream carrying flag configuration change notifications - content: - text/event-stream: - itemSchema: - $ref: "#/components/schemas/sseEvent" - responses: - "200": - description: SSE connection established successfully From 6db380254c3e40db5b2dddc501bba109cfdedf66 Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Mon, 13 Apr 2026 13:55:01 -0400 Subject: [PATCH 14/15] fix: add format date-time to lastModified, remove HTTP-date support Signed-off-by: Jonathan Norris --- service/adrs/0008-sse-for-bulk-evaluation-changes.md | 9 ++++----- service/openapi.yaml | 8 ++++---- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/service/adrs/0008-sse-for-bulk-evaluation-changes.md b/service/adrs/0008-sse-for-bulk-evaluation-changes.md index c142e81..91b6393 100644 --- a/service/adrs/0008-sse-for-bulk-evaluation-changes.md +++ b/service/adrs/0008-sse-for-bulk-evaluation-changes.md @@ -86,7 +86,7 @@ Providers must inspect `data.type` to determine behavior — not the SSE envelop Event data fields: - `type` (string, required): The OFREP event type inside the JSON data payload. Providers must handle `refetchEvaluation` and must ignore unknown values for forward compatibility. - `etag` (string, optional): Latest flag configuration cache validation token sent over SSE metadata. If present, providers should include it as the `flagConfigEtag` query parameter on the re-fetch request. -- `lastModified` (string | integer, optional): Latest flag configuration timestamp sent over SSE metadata. Supports either Unix timestamp in seconds (recommended) or a date string (ISO 8601 or HTTP-date). If present, providers should include it as the `flagConfigLastModified` query parameter on the re-fetch request. +- `lastModified` (string | integer, optional): Latest flag configuration timestamp sent over SSE metadata. Supports either Unix timestamp in seconds (recommended) or an ISO 8601 date-time string. If present, providers should include it as the `flagConfigLastModified` query parameter on the re-fetch request. For all provider types, a `refetchEvaluation` event means that the underlying flag configuration has changed. How the provider responds may differ by provider model, but the event semantics are the same. @@ -173,8 +173,8 @@ Provider implementation guidelines: name: flagConfigLastModified description: | Optional SSE-provided last-modified metadata for SSE-triggered re-fetches. - Supports Unix timestamp in seconds (recommended) or a date string (ISO 8601 / - HTTP-date), and is transported as query metadata rather than + Supports Unix timestamp in seconds (recommended) or an ISO 8601 date-time + string, and is transported as query metadata rather than `If-Modified-Since`. It should only be included when the request is directly triggered by a received SSE message. schema: @@ -182,14 +182,13 @@ Provider implementation guidelines: - type: integer minimum: 0 - type: string + format: date-time required: false examples: epochSeconds: value: 1771622898 isoDate: value: "2026-02-20T21:28:18Z" - httpDate: - value: "Fri, 20 Feb 2026 21:28:18 GMT" # Add to bulkEvaluationSuccess.properties: eventStreams: diff --git a/service/openapi.yaml b/service/openapi.yaml index 7533413..ae41aa6 100644 --- a/service/openapi.yaml +++ b/service/openapi.yaml @@ -295,7 +295,7 @@ components: description: | Optional last-modified metadata provided by an event stream for change-triggered re-fetches (see ADR-0008). Supports Unix timestamp in - seconds (recommended) or a date string (ISO 8601 / HTTP-date), and is + seconds (recommended) or an ISO 8601 date-time string, and is transported as query metadata rather than `If-Modified-Since`. It should only be included when the request is directly triggered by a received change notification event. @@ -304,14 +304,13 @@ components: - type: integer minimum: 0 - type: string + format: date-time required: false examples: epochSeconds: value: 1771622898 isoDate: value: "2026-02-20T21:28:18Z" - httpDate: - value: "Fri, 20 Feb 2026 21:28:18 GMT" schemas: bulkEvaluationRequest: description: | @@ -523,13 +522,14 @@ components: lastModified: description: | Latest flag configuration timestamp. Supports Unix timestamp in - seconds (recommended) or a date string (ISO 8601 or HTTP-date). + seconds (recommended) or an ISO 8601 date-time string. If present, providers should include it as the `flagConfigLastModified` query parameter on the re-fetch request. oneOf: - type: integer minimum: 0 - type: string + format: date-time example: 1771622898 evaluationSuccess: description: | From 807e3aa285bf3a9504f177095660eb0ec600d640 Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Mon, 13 Apr 2026 15:25:12 -0400 Subject: [PATCH 15/15] refactor: split event-streams into separate 3.2.0 file, revert main spec to 3.1.0 Signed-off-by: Jonathan Norris --- .spectral.yaml | 3 --- service/event-streams.yaml | 47 ++++++++++++++++++++++++++++++++++++++ service/openapi.yaml | 26 +-------------------- 3 files changed, 48 insertions(+), 28 deletions(-) create mode 100644 service/event-streams.yaml diff --git a/.spectral.yaml b/.spectral.yaml index a476373..77292bf 100644 --- a/.spectral.yaml +++ b/.spectral.yaml @@ -2,6 +2,3 @@ extends: - spectral:oas rules: oas3-valid-media-example: off - # Disabled until spectral supports OpenAPI 3.2.0 - # See: https://github.com/stoplightio/spectral/issues/2910 - oas3-schema: off diff --git a/service/event-streams.yaml b/service/event-streams.yaml new file mode 100644 index 0000000..e0ef1ec --- /dev/null +++ b/service/event-streams.yaml @@ -0,0 +1,47 @@ +openapi: 3.2.0 +servers: + - url: / +info: + version: 0.3.0 + title: OFREP Event Streams + description: | + Supplementary OpenAPI 3.2.0 specification documenting the Server-Sent Events (SSE) + event stream format used by OFREP for real-time flag configuration change notifications + (see ADR-0008). + + This file uses OpenAPI 3.2.0 for `text/event-stream` + `itemSchema` support. The core + OFREP specification remains at OpenAPI 3.1.0 in `openapi.yaml`. + + The SSE endpoint URLs are opaque and vendor-provided via the `eventStreams` field in + evaluation responses. This path item documents the event format only. + contact: + url: https://github.com/open-feature/protocol + license: + identifier: Apache-2.0 + name: Apache 2.0 +tags: + - name: Event Streams + description: | + **Optional**: Real-time change notification mechanisms for flag configuration updates (see ADR-0008). +components: + pathItems: + eventStreamConnection: + get: + tags: [Event Streams] + summary: SSE Flag Configuration Change Notification + description: | + Describes the Server-Sent Events (SSE) stream that providers receive from + the URLs specified in `eventStreams`. Events signal that the underlying flag + configuration has changed and the provider should re-fetch evaluations. + + This path item documents the event format only; the actual SSE endpoint URLs + are opaque and vendor-provided via the `eventStreams` field in evaluation + responses. Providers should connect to the URL returned in `eventStreams` + rather than constructing a URL from this path item. + responses: + "200": + description: SSE event stream connection established successfully + content: + text/event-stream: + itemSchema: + $ref: "openapi.yaml#/components/schemas/sseEvent" diff --git a/service/openapi.yaml b/service/openapi.yaml index ae41aa6..1707928 100644 --- a/service/openapi.yaml +++ b/service/openapi.yaml @@ -1,4 +1,4 @@ -openapi: 3.2.0 +openapi: 3.1.0 servers: - url: / info: @@ -28,9 +28,6 @@ tags: description: | **Required**: Core APIs to implement to support OFREP. *This is the minimum set of APIs required for a flag management system to be OFREP compatible.* - - name: Event Streams - description: | - **Optional**: Real-time change notification mechanisms for flag configuration updates (see ADR-0008). paths: /ofrep/v1/evaluate/flags/{key}: @@ -254,27 +251,6 @@ components: type: apiKey in: header name: X-API-Key - pathItems: - eventStreamConnection: - get: - tags: [Event Streams] - summary: SSE Flag Configuration Change Notification - description: | - Describes the Server-Sent Events (SSE) stream that providers receive from - the URLs specified in `eventStreams`. Events signal that the underlying flag - configuration has changed and the provider should re-fetch evaluations. - - This path item documents the event format only; the actual SSE endpoint URLs - are opaque and vendor-provided via the `eventStreams` field in evaluation - responses. Providers should connect to the URL returned in `eventStreams` - rather than constructing a URL from this path item. - responses: - "200": - description: SSE event stream connection established successfully - content: - text/event-stream: - itemSchema: - $ref: "#/components/schemas/sseEvent" parameters: flagConfigEtag: in: query