Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .spectral.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
jonathannorris marked this conversation as resolved.
Outdated
26 changes: 14 additions & 12 deletions service/adrs/0008-sse-for-bulk-evaluation-changes.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
Expand All @@ -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
{
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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: |
Expand Down Expand Up @@ -189,9 +189,9 @@ 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"
Comment thread
jonathannorris marked this conversation as resolved.
Outdated

# Add to bulkEvaluationSuccess.properties and serverEvaluationSuccess.properties:
# Add to bulkEvaluationSuccess.properties:
eventStreams:
type: array
description: |
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -322,5 +324,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.
231 changes: 227 additions & 4 deletions service/openapi.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
openapi: 3.1.0
openapi: 3.2.0
Comment thread
jonathannorris marked this conversation as resolved.
Outdated
Comment thread
jonathannorris marked this conversation as resolved.
Outdated
servers:
- url: /
info:
version: 0.2.0
version: 0.3.0
title: OpenFeature Remote Evaluation Protocol (OFREP)
description: |
---
Expand All @@ -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}:
Expand Down Expand Up @@ -146,7 +149,9 @@ paths:
schema:
type: string
required: false
example: '"abc123xyz"'
example: abc123xyz
Comment thread
jonathannorris marked this conversation as resolved.
- $ref: "#/components/parameters/flagConfigEtag"
- $ref: "#/components/parameters/flagConfigLastModified"
Comment thread
jonathannorris marked this conversation as resolved.
requestBody:
required: true
content:
Expand All @@ -170,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
Comment thread
jonathannorris marked this conversation as resolved.
Comment thread
jonathannorris marked this conversation as resolved.
content:
application/json:
schema:
Expand All @@ -188,6 +193,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":
Expand Down Expand Up @@ -245,6 +254,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
Comment thread
jonathannorris marked this conversation as resolved.
flagConfigLastModified:
Comment thread
jonathannorris marked this conversation as resolved.
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: "Fri, 20 Feb 2026 21:28:18 GMT"
schemas:
bulkEvaluationRequest:
description: |
Expand Down Expand Up @@ -276,6 +322,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
Expand Down Expand Up @@ -308,6 +364,152 @@ 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:
Comment thread
jonathannorris marked this conversation as resolved.
$ref: "#/components/schemas/eventStreamEndpoint"
inactivityDelaySec:
type: integer
minimum: 1
Comment thread
jonathannorris marked this conversation as resolved.
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
Comment thread
jonathannorris marked this conversation as resolved.
Comment thread
jonathannorris marked this conversation as resolved.
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
Comment thread
jonathannorris marked this conversation as resolved.
example: 1771622898
evaluationSuccess:
description: |
Successful feature flag evaluation response. The value property is present
Expand Down Expand Up @@ -498,3 +700,24 @@ components:
flagMetadataDescription:
description: |
Arbitrary metadata for the flag, useful for telemetry and documentary purposes.
webhooks:
Comment thread
lukas-reining marked this conversation as resolved.
Outdated
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
Loading