diff --git a/service/adrs/0008-sse-for-bulk-evaluation-changes.md b/service/adrs/0008-sse-for-bulk-evaluation-changes.md index 112d62a..91b6393 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 — 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. 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 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 @@ -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 { @@ -62,10 +62,10 @@ Add an optional `eventStreams` field to `bulkEvaluationSuccess` and `serverEvalu 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. @@ -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. @@ -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: | @@ -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,16 +182,15 @@ 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: "Thu, 20 Feb 2026 21:28:18 GMT" -# Add to bulkEvaluationSuccess.properties and serverEvaluationSuccess.properties: +# Add to bulkEvaluationSuccess.properties: eventStreams: type: array description: | @@ -243,18 +242,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 @@ -322,5 +323,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/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 f04804e..1707928 100644 --- a/service/openapi.yaml +++ b/service/openapi.yaml @@ -2,7 +2,7 @@ openapi: 3.1.0 servers: - url: / info: - version: 0.2.0 + version: 0.3.0 title: OpenFeature Remote Evaluation Protocol (OFREP) description: | --- @@ -146,7 +146,9 @@ paths: schema: type: string required: false - example: '"abc123xyz"' + example: abc123xyz + - $ref: "#/components/parameters/flagConfigEtag" + - $ref: "#/components/parameters/flagConfigLastModified" requestBody: required: true content: @@ -170,7 +172,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: @@ -188,6 +190,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": @@ -245,6 +251,42 @@ 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 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. + schema: + oneOf: + - type: integer + minimum: 0 + - type: string + format: date-time + required: false + examples: + epochSeconds: + value: 1771622898 + isoDate: + value: "2026-02-20T21:28:18Z" schemas: bulkEvaluationRequest: description: | @@ -276,6 +318,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 +360,153 @@ components: serverEvaluationSuccess: allOf: - $ref: "#/components/schemas/evaluationSuccess" + 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 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: + $ref: "#/components/schemas/eventStreamEndpoint" + 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 + 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: + - 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`. 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 + pattern: "^/" + description: | + The path + query portion of the endpoint URL. Must start with `/`. + 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 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: | Successful feature flag evaluation response. The value property is present @@ -498,3 +697,4 @@ components: flagMetadataDescription: description: | Arbitrary metadata for the flag, useful for telemetry and documentary purposes. +