From 44790c947049e3e24aae36dcd45d48e5e1c472e4 Mon Sep 17 00:00:00 2001 From: Norris Date: Fri, 20 Feb 2026 11:21:26 -0500 Subject: [PATCH 01/26] docs: add ADR for SSE bulk evaluation change notifications Signed-off-by: Norris --- .../0008-sse-for-bulk-evaluation-changes.md | 244 ++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 service/adrs/0008-sse-for-bulk-evaluation-changes.md diff --git a/service/adrs/0008-sse-for-bulk-evaluation-changes.md b/service/adrs/0008-sse-for-bulk-evaluation-changes.md new file mode 100644 index 0000000..0b4f920 --- /dev/null +++ b/service/adrs/0008-sse-for-bulk-evaluation-changes.md @@ -0,0 +1,244 @@ +# 8. Server-Sent Events (SSE) for bulk evaluation changes + +Date: 2026-02-20 + +## Status + +Proposed + +## Context + +OFREP currently relies exclusively on polling for flag change detection in client-side (static context) providers. 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. + +Polling has known limitations: +- There is no way to implement real-time flag updates +- Frequent polling introduces unnecessary load on flag management systems +- There is an inherent latency between flag changes and client awareness, bounded by the poll interval + +The [vendor survey](https://docs.google.com/forms/d/1NzqKx57XvRK_2lRQOFCRmF5exet6f15-sCjdEy0HCS8#responses) referenced in ADR-0005 confirmed that many vendors already use SSE for change notification. Without a standardized mechanism in OFREP, each vendor must implement proprietary push solutions, undermining the protocol's goal of vendor-agnostic interoperability. + +Server-Sent Events (SSE) is a W3C standard that fits this use case well: +- Unidirectional (server-to-client), matching the notification pattern +- Runs over standard HTTP without protocol upgrades +- Natively supported in browsers via the `EventSource` API +- Built-in reconnection support; when events include `id`, reconnecting clients can send `Last-Event-ID` to resume from the last processed event +- Works through proxies, CDNs, and standard HTTP infrastructure + +## Decision + +Add an optional `sse` array to the bulk evaluation response (`POST /ofrep/v1/evaluate/flags`). When present, it provides SSE endpoint URLs 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 the bulk evaluation via the existing endpoint, 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 `sse` field to `bulkEvaluationSuccess`: + +```json +{ + "flags": [ + { + "key": "discount-banner", + "value": true, + "reason": "TARGETING_MATCH", + "variant": "enabled" + } + ], + "sse": [ + { + "url": "https://sse.example.com/event-stream?channels=env_abc123_v1", + "inactivityDelaySec": 120 + } + ], + "metadata": { + "version": "v12" + } +} +``` + +Each SSE connection object has: +- `url` (string, required): The SSE endpoint URL. The URL is opaque to the provider and may include authentication tokens, channel identifiers, or other vendor-specific query parameters. +- `inactivityDelaySec` (integer, optional): Seconds of client inactivity (e.g., browser tab or mobile app backgrounded) after which the SSE connection should be closed. The client must reconnect and re-fetch when activity resumes. + +The `sse` 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. + +### SSE Event Format + +Events use the standard [SSE event format](https://html.spec.whatwg.org/multipage/server-sent-events.html) with a JSON `data` field: + +``` +id: evt-1234 +event: message +data: {"type": "refetchEvaluation", "etag": "\"abc123\"", "lastModified": 1771622898} +``` + +Event data fields: +- `type` (string, required): The event type. Providers must handle `refetchEvaluation` and must ignore unknown types for forward compatibility. +- `etag` (string, optional): Latest flag configuration validator sent over SSE metadata. If present, providers should include it as the `sseEtag` 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 `sseLastModified` query parameter on the re-fetch request. + +SSE envelope fields: +- `id` (string, recommended): Event identifier used by SSE clients for resume semantics via `Last-Event-ID`. + +Reconnection and replay behavior: +- Providers should rely on standard SSE reconnect behavior and pass `Last-Event-ID` when supported by the client/runtime. +- Servers that support replay should emit stable event `id` values for `refetchEvaluation` events and replay missed events when `Last-Event-ID` is provided. +- Providers must perform an immediate bulk re-fetch after reconnect, even when replay is supported, to guarantee cache correctness across implementations with different replay retention policies. + +Transporting SSE metadata to the bulk endpoint: +- `sseEtag` and `sseLastModified` are SSE-trigger metadata, not standard HTTP conditional request validators for endpoint-level response caching semantics. +- `sseEtag` and `sseLastModified` should only be sent when the re-fetch request is directly triggered by a received SSE message. +- For browser-based SDKs, query parameters avoid CORS preflight costs that would be introduced by custom headers. +- The metadata originates from the SSE channel, so query parameters make the source and intent explicit. +- This is particularly useful for implementations where the OFREP server validates internal cache state and storage freshness directly (for example, cache + object storage bindings) rather than forwarding conditional headers upstream. +- To reduce cross-language date parsing ambiguity, providers and servers should prefer Unix timestamp seconds for `lastModified` / `sseLastModified` when possible. + +### Provider Behavior + +```mermaid +sequenceDiagram + participant Client as OFREP Provider + participant Server as Flag Management System + participant SSE as SSE Endpoint + + Client->>Server: POST /ofrep/v1/evaluate/flags + Server-->>Client: 200 OK (flags + sse URLs + ETag) + Client->>Client: Cache flags, store ETag + Client->>SSE: Connect to SSE URL(s) + + Note over SSE,Client: Real-time change notification + SSE-->>Client: event: refetchEvaluation (etag, lastModified) + Client->>Server: POST /ofrep/v1/evaluate/flags?sseEtag=etag&sseLastModified=lastModified + alt Flags changed + Server-->>Client: 200 OK (new flags + ETag) + Client->>Client: Update cache, emit ConfigurationChanged + else Flags unchanged + Server-->>Client: 304 Not Modified + end + + Note over Client: Browser tab backgrounded + Client->>SSE: Close connection (after inactivityDelaySec) + Note over Client: Browser tab foregrounded + Client->>SSE: Reconnect to SSE URL(s) + Client->>Server: POST /ofrep/v1/evaluate/flags +``` + +Provider implementation guidelines: +1. After the initial bulk evaluation response, if `sse` is present, the provider should connect to the provided URL(s). +2. On receiving a `refetchEvaluation` event, the provider must re-fetch flag evaluations from the bulk evaluation endpoint. If `etag` is present, it should be sent as `sseEtag` query parameter. If `lastModified` is present, it should be sent as `sseLastModified` query parameter. These query parameters should only be included for requests directly triggered by processing that SSE event. + `lastModified` parsing should support Unix timestamp seconds and date string formats. +3. If `inactivityDelaySec` is specified, the provider should close the SSE connection after the specified inactivity period. On resumption, it must reconnect and immediately re-fetch without SSE query metadata. +4. If the SSE connection fails or is unavailable, the provider must fall back to its configured change detection behavior: if polling is enabled, continue with polling; if polling is disabled, continue SSE reconnection attempts and rely on explicit refresh triggers such as `onContextChange`. +5. Providers should implement reconnection with exponential backoff. The native `EventSource` API in browsers handles this automatically. +6. When `onContextChange` is triggered, the provider re-fetches the bulk evaluation without SSE query metadata. The SSE URL(s) in the new response may differ, and the provider must update its connections accordingly. + +### OpenAPI Schema Additions + +```yaml +# Add to /ofrep/v1/evaluate/flags POST parameters: +- in: query + name: sseEtag + description: | + Optional SSE-provided ETag metadata for SSE-triggered re-fetches. This is + not a standard HTTP conditional request header; it is metadata for server-side + cache validation and freshness checks initiated by SSE events. It should only + be included when the request is directly triggered by a received SSE message. + schema: + type: string + required: false + example: "\"550e8400-e29b-41d4-a716-446655440000\"" + +- in: query + name: sseLastModified + description: | + Optional SSE-provided last-modified metadata for SSE-triggered re-fetches. + Supports Unix timestamp 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 SSE message. + 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" + +# Add to bulkEvaluationSuccess.properties: +sse: + type: array + description: | + Optional array of SSE (Server-Sent Events) endpoints the client can connect + to for real-time flag change notifications. When present, the provider should + connect to these endpoints and re-fetch flag evaluations when notified of changes. + If not present, the provider should continue using polling for change detection. + items: + $ref: "#/components/schemas/sseConnection" + +# Add to components.schemas: +sseConnection: + description: | + An SSE connection endpoint for receiving real-time flag change notifications. + type: object + required: + - url + properties: + url: + type: string + format: uri + description: | + The SSE endpoint URL the client should connect to for real-time + flag change notifications. The URL may include authentication tokens, + channel identifiers, or other query parameters as needed by the + vendor's SSE infrastructure. + example: "https://sse.example.com/event-stream?channels=env_abc123_v1" + inactivityDelaySec: + type: integer + minimum: 0 + description: | + Number of seconds of client inactivity after which the SSE connection + should be closed to conserve resources. The client must reconnect + when activity resumes. If omitted or 0, the connection should be + maintained indefinitely. + example: 120 +``` + +## Consequences + +### Positive + +- **Real-time flag updates**: Providers can receive flag change notifications immediately rather than waiting for the next poll interval +- **Reduced server load**: Eliminates unnecessary polling requests when flags have not changed +- **Vendor-agnostic**: The `url` field is opaque, allowing vendors to use any SSE infrastructure (hosted services like Ably/Pusher, self-hosted endpoints, CDN-based proxies) +- **Backward compatible**: The `sse` field is fully optional -- servers that don't support it omit the field, providers that don't support it ignore the field and continue their configured change detection behavior +- **Builds on existing infrastructure**: Uses the existing bulk evaluation endpoint for data transfer, keeping SSE as a lightweight notification layer + +### Negative + +- **Additional provider complexity**: Providers must manage SSE connection lifecycle, reconnection, inactivity handling, and fallback behavior based on configured change detection settings +- **Infrastructure requirements**: Flag management systems that want to support SSE need to operate or integrate with an SSE-capable service +- **Connection resource usage**: Long-lived SSE connections consume resources on both client and server, particularly at scale +- **Re-fetch amplification risk**: Multiple SSE URLs or bursty event streams can trigger redundant concurrent re-fetches unless providers coalesce events +- **Transport consistency trade-off**: Using query parameters for SSE metadata differs from common HTTP conditional request patterns and may need careful documentation for implementers +- **Tokenized URL handling risk**: If SSE URLs include scoped credentials or channel tokens, accidental logging/persistence can expose sensitive connection material + +## Open Questions + +1. **Should `refetchEvaluation` be required, or should providers refetch on any SSE message?** Requiring a specific `type` field enables future event types without triggering unnecessary refetches. Refetching on any message is simpler. This ADR recommends requiring `type=refetchEvaluation` for forward compatibility. +2. **Should providers support streaming full evaluation payloads over SSE?** This ADR focuses on the notification pattern. Full payload streaming could be specified as a separate event type in a future revision. +3. **What is the recommended coalescing strategy when multiple SSE connections are specified?** Providers should connect to all URLs, but should re-fetch in a coalesced way (debounce + in-flight dedupe) to avoid amplification. Should OFREP define minimum coalescing expectations? +4. **Should `inactivityDelaySec` be server-provided or client-side configuration?** This ADR specifies it as server-provided, allowing the server to tune connection lifecycle. Providers may also expose a client-side override. +5. **Should non-`refetchEvaluation` SSE messages be forwarded to the provider?** Should we add a mechanism to support non-`refetchEvaluation` typed messages that are forwarded through to the provider via an events/hook interface? +6. **Should SSE metadata be transported via query parameters or custom headers?** This ADR currently recommends query params (`sseEtag`, `sseLastModified`) due to browser CORS preflight considerations and the non-conditional-request semantics. Should OFREP also define an optional custom header form for non-browser clients? +7. **What security requirements should apply to tokenized SSE URLs?** Should OFREP require URL redaction in logs/telemetry, recommend short-lived scoped tokens, and discourage long-term persistence of raw SSE URLs? + +## Implementation Notes + +- **Existing SSE libraries**: The LaunchDarkly open-source SSE client libraries ([Java](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)) are well-maintained and could be used by OFREP provider implementations. Browser environments can use the native `EventSource` API. +- **Static context provider guideline update**: The [static context provider guideline](../../guideline/static-context-provider.md) would need a new section describing SSE connection management alongside the existing polling section. From 99bffdfb5d9b6d2784d6d20258b709aadc33af06 Mon Sep 17 00:00:00 2001 From: Norris Date: Fri, 20 Feb 2026 15:21:13 -0500 Subject: [PATCH 02/26] docs: clarify ADR SSE scope for static-context providers Signed-off-by: Norris --- service/adrs/0008-sse-for-bulk-evaluation-changes.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/service/adrs/0008-sse-for-bulk-evaluation-changes.md b/service/adrs/0008-sse-for-bulk-evaluation-changes.md index 0b4f920..da85af9 100644 --- a/service/adrs/0008-sse-for-bulk-evaluation-changes.md +++ b/service/adrs/0008-sse-for-bulk-evaluation-changes.md @@ -10,6 +10,8 @@ Proposed OFREP currently relies exclusively on polling for flag change detection in client-side (static context) providers. 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 focuses primarily on static-context providers (for example web and mobile SDK providers) that use bulk evaluation caching patterns. It does not introduce SSE requirements for dynamic-context providers that primarily use single-flag evaluations. + Polling has known limitations: - There is no way to implement real-time flag updates - Frequent polling introduces unnecessary load on flag management systems @@ -27,6 +29,7 @@ Server-Sent Events (SSE) is a W3C standard that fits this use case well: ## Decision Add an optional `sse` array to the bulk evaluation response (`POST /ofrep/v1/evaluate/flags`). When present, it provides SSE endpoint URLs that the provider connects to for real-time flag change notifications. +This is primarily intended for static-context providers (for example web/mobile clients) that rely on bulk evaluations. SSE is used as a **notification-only** mechanism -- events signal the provider to re-fetch the bulk evaluation via the existing endpoint, rather than streaming full evaluation payloads. This keeps the SSE message format simple, reuses existing infrastructure, and avoids duplicating evaluation logic. @@ -175,9 +178,11 @@ sse: type: array description: | Optional array of SSE (Server-Sent Events) endpoints the client can connect - to for real-time flag change notifications. When present, the provider should - connect to these endpoints and re-fetch flag evaluations when notified of changes. - If not present, the provider should continue using polling for change detection. + to for real-time flag change notifications. This is primarily intended for + static-context providers (for example web/mobile providers) using bulk + evaluation caching patterns. When present, the provider should connect to + these endpoints and re-fetch flag evaluations when notified of changes. If + not present, the provider should continue using polling for change detection. items: $ref: "#/components/schemas/sseConnection" From 45df3d04025b5d04ee0794bb58cae56ec346563e Mon Sep 17 00:00:00 2001 From: Norris Date: Fri, 27 Feb 2026 14:16:26 -0500 Subject: [PATCH 03/26] docs: rename sse response field to refreshConnections with type discriminator Signed-off-by: Norris --- .../0008-sse-for-bulk-evaluation-changes.md | 60 +++++++++++-------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/service/adrs/0008-sse-for-bulk-evaluation-changes.md b/service/adrs/0008-sse-for-bulk-evaluation-changes.md index da85af9..20540a9 100644 --- a/service/adrs/0008-sse-for-bulk-evaluation-changes.md +++ b/service/adrs/0008-sse-for-bulk-evaluation-changes.md @@ -28,14 +28,14 @@ Server-Sent Events (SSE) is a W3C standard that fits this use case well: ## Decision -Add an optional `sse` array to the bulk evaluation response (`POST /ofrep/v1/evaluate/flags`). When present, it provides SSE endpoint URLs that the provider connects to for real-time flag change notifications. +Add an optional `refreshConnections` 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. This is primarily intended for static-context providers (for example web/mobile clients) that rely on bulk evaluations. SSE is used as a **notification-only** mechanism -- events signal the provider to re-fetch the bulk evaluation via the existing endpoint, 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 `sse` field to `bulkEvaluationSuccess`: +Add an optional `refreshConnections` field to `bulkEvaluationSuccess`: ```json { @@ -47,8 +47,9 @@ Add an optional `sse` field to `bulkEvaluationSuccess`: "variant": "enabled" } ], - "sse": [ + "refreshConnections": [ { + "type": "sse", "url": "https://sse.example.com/event-stream?channels=env_abc123_v1", "inactivityDelaySec": 120 } @@ -59,11 +60,12 @@ Add an optional `sse` field to `bulkEvaluationSuccess`: } ``` -Each SSE connection object has: -- `url` (string, required): The SSE endpoint URL. The URL is opaque to the provider and may include authentication tokens, channel identifiers, or other vendor-specific query parameters. -- `inactivityDelaySec` (integer, optional): Seconds of client inactivity (e.g., browser tab or mobile app backgrounded) after which the SSE connection should be closed. The client must reconnect and re-fetch when activity resumes. +Each refresh connection 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, required): The endpoint URL. The URL is opaque to the provider and may include authentication tokens, channel identifiers, or other vendor-specific query parameters. +- `inactivityDelaySec` (integer, optional): Seconds of client inactivity (e.g., browser tab or mobile app backgrounded) after which the connection should be closed. The client must reconnect and re-fetch when activity resumes. -The `sse` 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. +The `refreshConnections` 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. ### SSE Event Format @@ -105,7 +107,7 @@ sequenceDiagram participant SSE as SSE Endpoint Client->>Server: POST /ofrep/v1/evaluate/flags - Server-->>Client: 200 OK (flags + sse URLs + ETag) + Server-->>Client: 200 OK (flags + refreshConnections + ETag header) Client->>Client: Cache flags, store ETag Client->>SSE: Connect to SSE URL(s) @@ -127,13 +129,13 @@ sequenceDiagram ``` Provider implementation guidelines: -1. After the initial bulk evaluation response, if `sse` is present, the provider should connect to the provided URL(s). +1. After the initial bulk evaluation response, if `refreshConnections` is present, the provider should connect to any entries with a known `type` (currently `"sse"`). 2. On receiving a `refetchEvaluation` event, the provider must re-fetch flag evaluations from the bulk evaluation endpoint. If `etag` is present, it should be sent as `sseEtag` query parameter. If `lastModified` is present, it should be sent as `sseLastModified` query parameter. These query parameters should only be included for requests directly triggered by processing that SSE event. `lastModified` parsing should support Unix timestamp seconds and date string formats. 3. If `inactivityDelaySec` is specified, the provider should close the SSE connection after the specified inactivity period. On resumption, it must reconnect and immediately re-fetch without SSE query metadata. 4. If the SSE connection fails or is unavailable, the provider must fall back to its configured change detection behavior: if polling is enabled, continue with polling; if polling is disabled, continue SSE reconnection attempts and rely on explicit refresh triggers such as `onContextChange`. 5. Providers should implement reconnection with exponential backoff. The native `EventSource` API in browsers handles this automatically. -6. When `onContextChange` is triggered, the provider re-fetches the bulk evaluation without SSE query metadata. The SSE URL(s) in the new response may differ, and the provider must update its connections accordingly. +6. When `onContextChange` is triggered, the provider re-fetches the bulk evaluation without SSE query metadata. The `refreshConnections` in the new response may differ, and the provider must update its connections accordingly. ### OpenAPI Schema Additions @@ -174,40 +176,50 @@ Provider implementation guidelines: value: "Thu, 20 Feb 2026 21:28:18 GMT" # Add to bulkEvaluationSuccess.properties: -sse: +refreshConnections: type: array description: | - Optional array of SSE (Server-Sent Events) endpoints the client can connect - to for real-time flag change notifications. This is primarily intended for - static-context providers (for example web/mobile providers) using bulk - evaluation caching patterns. When present, the provider should connect to - these endpoints and re-fetch flag evaluations when notified of changes. If - not present, the provider should continue using polling for change detection. + Optional array of real-time change notification connections. This is primarily + intended for static-context providers (for example web/mobile providers) using + bulk evaluation caching patterns. 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/sseConnection" + $ref: "#/components/schemas/refreshConnection" # Add to components.schemas: -sseConnection: +refreshConnection: description: | - An SSE connection endpoint for receiving real-time flag change notifications. + 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. type: object required: + - type - 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 SSE endpoint URL the client should connect to for real-time + The endpoint URL the client should connect to for real-time flag change notifications. The URL may include authentication tokens, channel identifiers, or other query parameters as needed by the - vendor's SSE infrastructure. + vendor's infrastructure. example: "https://sse.example.com/event-stream?channels=env_abc123_v1" inactivityDelaySec: type: integer minimum: 0 description: | - Number of seconds of client inactivity after which the SSE connection + Number of seconds of client inactivity after which the connection should be closed to conserve resources. The client must reconnect when activity resumes. If omitted or 0, the connection should be maintained indefinitely. @@ -221,7 +233,7 @@ sseConnection: - **Real-time flag updates**: Providers can receive flag change notifications immediately rather than waiting for the next poll interval - **Reduced server load**: Eliminates unnecessary polling requests when flags have not changed - **Vendor-agnostic**: The `url` field is opaque, allowing vendors to use any SSE infrastructure (hosted services like Ably/Pusher, self-hosted endpoints, CDN-based proxies) -- **Backward compatible**: The `sse` field is fully optional -- servers that don't support it omit the field, providers that don't support it ignore the field and continue their configured change detection behavior +- **Backward compatible**: The `refreshConnections` field is fully optional -- servers that don't support it omit the field, providers that don't support it ignore the field and continue their configured change detection behavior - **Builds on existing infrastructure**: Uses the existing bulk evaluation endpoint for data transfer, keeping SSE as a lightweight notification layer ### Negative From 214980ecf81b7cce128bd49b50723aac76482b2d Mon Sep 17 00:00:00 2001 From: Norris Date: Fri, 27 Feb 2026 14:18:29 -0500 Subject: [PATCH 04/26] docs: clarify post-inactivity re-fetch is fully unconditional Signed-off-by: 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 20540a9..642af8f 100644 --- a/service/adrs/0008-sse-for-bulk-evaluation-changes.md +++ b/service/adrs/0008-sse-for-bulk-evaluation-changes.md @@ -132,7 +132,7 @@ Provider implementation guidelines: 1. After the initial bulk evaluation response, if `refreshConnections` is present, the provider should connect to any entries with a known `type` (currently `"sse"`). 2. On receiving a `refetchEvaluation` event, the provider must re-fetch flag evaluations from the bulk evaluation endpoint. If `etag` is present, it should be sent as `sseEtag` query parameter. If `lastModified` is present, it should be sent as `sseLastModified` query parameter. These query parameters should only be included for requests directly triggered by processing that SSE event. `lastModified` parsing should support Unix timestamp seconds and date string formats. -3. If `inactivityDelaySec` is specified, the provider should close the SSE connection after the specified inactivity period. On resumption, it must reconnect and immediately re-fetch without SSE query metadata. +3. If `inactivityDelaySec` is specified, the provider should close the SSE connection after the specified inactivity period. On resumption, it must reconnect and immediately perform a full unconditional re-fetch -- without `If-None-Match`, `sseEtag`, or `sseLastModified` -- to ensure the cache reflects the current server state after an unknown period of inactivity. 4. If the SSE connection fails or is unavailable, the provider must fall back to its configured change detection behavior: if polling is enabled, continue with polling; if polling is disabled, continue SSE reconnection attempts and rely on explicit refresh triggers such as `onContextChange`. 5. Providers should implement reconnection with exponential backoff. The native `EventSource` API in browsers handles this automatically. 6. When `onContextChange` is triggered, the provider re-fetches the bulk evaluation without SSE query metadata. The `refreshConnections` in the new response may differ, and the provider must update its connections accordingly. From 054d5dabc80eb8474c15ee9f10678df726f311e8 Mon Sep 17 00:00:00 2001 From: Norris Date: Fri, 27 Feb 2026 14:21:05 -0500 Subject: [PATCH 05/26] docs: add changeDetection provider config option with sse as default Signed-off-by: Norris --- service/adrs/0008-sse-for-bulk-evaluation-changes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/service/adrs/0008-sse-for-bulk-evaluation-changes.md b/service/adrs/0008-sse-for-bulk-evaluation-changes.md index 642af8f..67eaf94 100644 --- a/service/adrs/0008-sse-for-bulk-evaluation-changes.md +++ b/service/adrs/0008-sse-for-bulk-evaluation-changes.md @@ -257,5 +257,9 @@ refreshConnection: ## Implementation Notes +- **Provider change detection configuration**: Providers should expose a `changeDetection` configuration option with the following values: + - `sse` *(default)*: Use SSE if the bulk evaluation response includes a `refreshConnections` entry with `type: "sse"`, falling back to polling on connection failure. If no `refreshConnections` are present, polling is used. + - `polling`: Ignore `refreshConnections` 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](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)) are well-maintained and could be used by OFREP provider implementations. Browser environments can use the native `EventSource` API. - **Static context provider guideline update**: The [static context provider guideline](../../guideline/static-context-provider.md) would need a new section describing SSE connection management alongside the existing polling section. From 94b1fc6e6e74b770accd3fe1bc1a1643400d51b9 Mon Sep 17 00:00:00 2001 From: Norris Date: Fri, 27 Feb 2026 14:23:55 -0500 Subject: [PATCH 06/26] docs: set inactivityDelaySec minimum to 1, default to 120 when omitted Signed-off-by: Norris --- .../adrs/0008-sse-for-bulk-evaluation-changes.md | 13 +++++++------ 1 file changed, 7 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 67eaf94..cbbeaec 100644 --- a/service/adrs/0008-sse-for-bulk-evaluation-changes.md +++ b/service/adrs/0008-sse-for-bulk-evaluation-changes.md @@ -63,7 +63,7 @@ Add an optional `refreshConnections` field to `bulkEvaluationSuccess`: Each refresh connection 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, required): The endpoint URL. The URL is opaque to the provider and may include authentication tokens, channel identifiers, or other vendor-specific query parameters. -- `inactivityDelaySec` (integer, optional): Seconds of client inactivity (e.g., browser tab or mobile app backgrounded) after which the connection should be closed. The client must reconnect and re-fetch when activity resumes. +- `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`. If omitted, providers should default to `120` seconds. The `refreshConnections` 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. @@ -217,12 +217,13 @@ refreshConnection: example: "https://sse.example.com/event-stream?channels=env_abc123_v1" inactivityDelaySec: type: integer - minimum: 0 + minimum: 1 description: | - Number of seconds of client inactivity after which the connection - should be closed to conserve resources. The client must reconnect - when activity resumes. If omitted or 0, the connection should be - maintained indefinitely. + 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. If omitted, providers + should default to 120 seconds. example: 120 ``` From a030f62b2eb693c2567a98d5e2bddb4b8e6f97ac Mon Sep 17 00:00:00 2001 From: Norris Date: Fri, 27 Feb 2026 14:26:34 -0500 Subject: [PATCH 07/26] docs: clarify onContextChange SSE connection transition behavior Signed-off-by: Norris --- service/adrs/0008-sse-for-bulk-evaluation-changes.md | 5 ++++- 1 file changed, 4 insertions(+), 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 cbbeaec..872d9a3 100644 --- a/service/adrs/0008-sse-for-bulk-evaluation-changes.md +++ b/service/adrs/0008-sse-for-bulk-evaluation-changes.md @@ -135,7 +135,10 @@ Provider implementation guidelines: 3. If `inactivityDelaySec` is specified, the provider should close the SSE connection after the specified inactivity period. On resumption, it must reconnect and immediately perform a full unconditional re-fetch -- without `If-None-Match`, `sseEtag`, or `sseLastModified` -- to ensure the cache reflects the current server state after an unknown period of inactivity. 4. If the SSE connection fails or is unavailable, the provider must fall back to its configured change detection behavior: if polling is enabled, continue with polling; if polling is disabled, continue SSE reconnection attempts and rely on explicit refresh triggers such as `onContextChange`. 5. Providers should implement reconnection with exponential backoff. The native `EventSource` API in browsers handles this automatically. -6. When `onContextChange` is triggered, the provider re-fetches the bulk evaluation without SSE query metadata. The `refreshConnections` in the new response may differ, and the provider must update its connections accordingly. +6. When `onContextChange` is triggered, the provider re-fetches the bulk evaluation without SSE query metadata and updates its connections based on the new response: + - If `refreshConnections` is absent, close all existing connections and fall back to configured change detection behavior. + - If `refreshConnections` is present and the URL set is unchanged, existing connections may be reused. + - If `refreshConnections` is present and the URL set has changed, close existing connections then connect to the new URLs. ### OpenAPI Schema Additions From a82a373950fd10ea01b8799bfb3d25e27c768f15 Mon Sep 17 00:00:00 2001 From: Norris Date: Fri, 27 Feb 2026 14:27:45 -0500 Subject: [PATCH 08/26] docs: add security guidance to not log SSE connection URLs Signed-off-by: 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 872d9a3..0849544 100644 --- a/service/adrs/0008-sse-for-bulk-evaluation-changes.md +++ b/service/adrs/0008-sse-for-bulk-evaluation-changes.md @@ -62,7 +62,7 @@ Add an optional `refreshConnections` field to `bulkEvaluationSuccess`: Each refresh connection 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, required): The endpoint URL. The URL is opaque to the provider and may include authentication tokens, channel identifiers, or other vendor-specific query parameters. +- `url` (string, required): The endpoint URL. The URL is opaque to the provider and 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. - `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`. If omitted, providers should default to `120` seconds. The `refreshConnections` 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. From 9ba8795452defa49c0a0918b5e5ff5917d439b13 Mon Sep 17 00:00:00 2001 From: Norris Date: Fri, 27 Feb 2026 14:30:45 -0500 Subject: [PATCH 09/26] docs: clarify etag field description as cache validation token Signed-off-by: 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 0849544..6d37032 100644 --- a/service/adrs/0008-sse-for-bulk-evaluation-changes.md +++ b/service/adrs/0008-sse-for-bulk-evaluation-changes.md @@ -79,7 +79,7 @@ data: {"type": "refetchEvaluation", "etag": "\"abc123\"", "lastModified": 177162 Event data fields: - `type` (string, required): The event type. Providers must handle `refetchEvaluation` and must ignore unknown types for forward compatibility. -- `etag` (string, optional): Latest flag configuration validator sent over SSE metadata. If present, providers should include it as the `sseEtag` query parameter on the re-fetch request. +- `etag` (string, optional): Latest flag configuration cache validation token sent over SSE metadata. If present, providers should include it as the `sseEtag` 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 `sseLastModified` query parameter on the re-fetch request. SSE envelope fields: From 0706162f67f3e7361e358c06dbb05643b214ff16 Mon Sep 17 00:00:00 2001 From: Norris Date: Fri, 27 Feb 2026 14:36:01 -0500 Subject: [PATCH 10/26] docs: add SHOULD-level coalescing requirement for concurrent refetch events Signed-off-by: Norris --- service/adrs/0008-sse-for-bulk-evaluation-changes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/service/adrs/0008-sse-for-bulk-evaluation-changes.md b/service/adrs/0008-sse-for-bulk-evaluation-changes.md index 6d37032..f7d2620 100644 --- a/service/adrs/0008-sse-for-bulk-evaluation-changes.md +++ b/service/adrs/0008-sse-for-bulk-evaluation-changes.md @@ -139,6 +139,7 @@ Provider implementation guidelines: - If `refreshConnections` is absent, close all existing connections and fall back to configured change detection behavior. - If `refreshConnections` is present and the URL set is unchanged, existing connections may be reused. - If `refreshConnections` is present and the URL set has changed, close existing connections then connect to the new URLs. +7. Providers SHOULD coalesce concurrent `refetchEvaluation` events into a single re-fetch request (e.g., via in-flight deduplication or a short debounce window) to avoid amplifying load on the flag management system when multiple connections fire simultaneously. ### OpenAPI Schema Additions From 1c2a648a6efda103d572888f62bac6e3c6366e63 Mon Sep 17 00:00:00 2001 From: Norris Date: Fri, 27 Feb 2026 14:36:37 -0500 Subject: [PATCH 11/26] docs: consistent use of Unix timestamp in seconds phrasing Signed-off-by: Norris --- service/adrs/0008-sse-for-bulk-evaluation-changes.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/service/adrs/0008-sse-for-bulk-evaluation-changes.md b/service/adrs/0008-sse-for-bulk-evaluation-changes.md index f7d2620..47590ea 100644 --- a/service/adrs/0008-sse-for-bulk-evaluation-changes.md +++ b/service/adrs/0008-sse-for-bulk-evaluation-changes.md @@ -96,7 +96,7 @@ Transporting SSE metadata to the bulk endpoint: - For browser-based SDKs, query parameters avoid CORS preflight costs that would be introduced by custom headers. - The metadata originates from the SSE channel, so query parameters make the source and intent explicit. - This is particularly useful for implementations where the OFREP server validates internal cache state and storage freshness directly (for example, cache + object storage bindings) rather than forwarding conditional headers upstream. -- To reduce cross-language date parsing ambiguity, providers and servers should prefer Unix timestamp seconds for `lastModified` / `sseLastModified` when possible. +- To reduce cross-language date parsing ambiguity, providers and servers should prefer Unix timestamp in seconds for `lastModified` / `sseLastModified` when possible. ### Provider Behavior @@ -131,7 +131,7 @@ sequenceDiagram Provider implementation guidelines: 1. After the initial bulk evaluation response, if `refreshConnections` is present, the provider should connect to any entries with a known `type` (currently `"sse"`). 2. On receiving a `refetchEvaluation` event, the provider must re-fetch flag evaluations from the bulk evaluation endpoint. If `etag` is present, it should be sent as `sseEtag` query parameter. If `lastModified` is present, it should be sent as `sseLastModified` query parameter. These query parameters should only be included for requests directly triggered by processing that SSE event. - `lastModified` parsing should support Unix timestamp seconds and date string formats. + `lastModified` parsing should support Unix timestamp in seconds and date string formats. 3. If `inactivityDelaySec` is specified, the provider should close the SSE connection after the specified inactivity period. On resumption, it must reconnect and immediately perform a full unconditional re-fetch -- without `If-None-Match`, `sseEtag`, or `sseLastModified` -- to ensure the cache reflects the current server state after an unknown period of inactivity. 4. If the SSE connection fails or is unavailable, the provider must fall back to its configured change detection behavior: if polling is enabled, continue with polling; if polling is disabled, continue SSE reconnection attempts and rely on explicit refresh triggers such as `onContextChange`. 5. Providers should implement reconnection with exponential backoff. The native `EventSource` API in browsers handles this automatically. @@ -161,7 +161,7 @@ Provider implementation guidelines: name: sseLastModified description: | Optional SSE-provided last-modified metadata for SSE-triggered re-fetches. - Supports Unix timestamp seconds (recommended) or a date string (ISO 8601 / + 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 SSE message. From 475f74b89cd87929701552a7e553d3ac64ee7c94 Mon Sep 17 00:00:00 2001 From: Norris Date: Wed, 4 Mar 2026 22:46:45 +0100 Subject: [PATCH 12/26] docs: add Swift/iOS and Android SSE library links to implementation notes Signed-off-by: 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 47590ea..80cce90 100644 --- a/service/adrs/0008-sse-for-bulk-evaluation-changes.md +++ b/service/adrs/0008-sse-for-bulk-evaluation-changes.md @@ -266,5 +266,5 @@ refreshConnection: - `sse` *(default)*: Use SSE if the bulk evaluation response includes a `refreshConnections` entry with `type: "sse"`, falling back to polling on connection failure. If no `refreshConnections` are present, polling is used. - `polling`: Ignore `refreshConnections` 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](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)) are well-maintained and could be used by OFREP provider implementations. Browser environments can use the native `EventSource` API. +- **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. - **Static context provider guideline update**: The [static context provider guideline](../../guideline/static-context-provider.md) would need a new section describing SSE connection management alongside the existing polling section. From 4f30ff7a950968d08f347faf002ce92f4c074589 Mon Sep 17 00:00:00 2001 From: Norris Date: Thu, 5 Mar 2026 22:38:29 +0100 Subject: [PATCH 13/26] docs: clarify event: message is intentional and type lives in data payload Signed-off-by: Norris --- service/adrs/0008-sse-for-bulk-evaluation-changes.md | 6 +++++- 1 file changed, 5 insertions(+), 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 80cce90..71bc3b5 100644 --- a/service/adrs/0008-sse-for-bulk-evaluation-changes.md +++ b/service/adrs/0008-sse-for-bulk-evaluation-changes.md @@ -77,8 +77,12 @@ event: message data: {"type": "refetchEvaluation", "etag": "\"abc123\"", "lastModified": 1771622898} ``` +The SSE envelope `event:` field is always `message`. Using a named SSE event type (e.g. `event: refetchEvaluation`) was considered but rejected — most SSE client libraries (Java, Swift, .NET, Python) do not support registering handlers per named event type and require manual dispatch regardless, so routing via a `type` field inside the JSON `data` payload achieves the same result consistently across all implementations. It also makes ignoring unknown future event types trivial with a single generic handler. + +Providers must inspect `data.type` to determine behavior — not the SSE envelope `event:` field. + Event data fields: -- `type` (string, required): The event type. Providers must handle `refetchEvaluation` and must ignore unknown types for forward compatibility. +- `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 `sseEtag` 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 `sseLastModified` query parameter on the re-fetch request. From 208253d257c73f34a346ef054e97417989f00b7e Mon Sep 17 00:00:00 2001 From: Norris Date: Thu, 5 Mar 2026 23:06:05 +0100 Subject: [PATCH 14/26] docs: add error/stale scenario to SSE sequence diagram Signed-off-by: Norris --- service/adrs/0008-sse-for-bulk-evaluation-changes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/service/adrs/0008-sse-for-bulk-evaluation-changes.md b/service/adrs/0008-sse-for-bulk-evaluation-changes.md index 71bc3b5..ea33fd6 100644 --- a/service/adrs/0008-sse-for-bulk-evaluation-changes.md +++ b/service/adrs/0008-sse-for-bulk-evaluation-changes.md @@ -123,6 +123,10 @@ sequenceDiagram Client->>Client: Update cache, emit ConfigurationChanged else Flags unchanged Server-->>Client: 304 Not Modified + else Server not yet updated (stale) + Server-->>Client: 5xx or stale 200 + Note over Client,Server: Server may internally invalidate cache and retry
until updated value is available before responding + Client->>Client: Fall back to polling on persistent failure end Note over Client: Browser tab backgrounded From 737ddac21e99ea2d7804249038134d2ffe1206ff Mon Sep 17 00:00:00 2001 From: Norris Date: Thu, 5 Mar 2026 23:18:20 +0100 Subject: [PATCH 15/26] docs: add answers to open questions Signed-off-by: Norris --- .../0008-sse-for-bulk-evaluation-changes.md | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/service/adrs/0008-sse-for-bulk-evaluation-changes.md b/service/adrs/0008-sse-for-bulk-evaluation-changes.md index ea33fd6..5ebfdd9 100644 --- a/service/adrs/0008-sse-for-bulk-evaluation-changes.md +++ b/service/adrs/0008-sse-for-bulk-evaluation-changes.md @@ -260,13 +260,26 @@ refreshConnection: ## Open Questions -1. **Should `refetchEvaluation` be required, or should providers refetch on any SSE message?** Requiring a specific `type` field enables future event types without triggering unnecessary refetches. Refetching on any message is simpler. This ADR recommends requiring `type=refetchEvaluation` for forward compatibility. -2. **Should providers support streaming full evaluation payloads over SSE?** This ADR focuses on the notification pattern. Full payload streaming could be specified as a separate event type in a future revision. -3. **What is the recommended coalescing strategy when multiple SSE connections are specified?** Providers should connect to all URLs, but should re-fetch in a coalesced way (debounce + in-flight dedupe) to avoid amplification. Should OFREP define minimum coalescing expectations? -4. **Should `inactivityDelaySec` be server-provided or client-side configuration?** This ADR specifies it as server-provided, allowing the server to tune connection lifecycle. Providers may also expose a client-side override. -5. **Should non-`refetchEvaluation` SSE messages be forwarded to the provider?** Should we add a mechanism to support non-`refetchEvaluation` typed messages that are forwarded through to the provider via an events/hook interface? -6. **Should SSE metadata be transported via query parameters or custom headers?** This ADR currently recommends query params (`sseEtag`, `sseLastModified`) due to browser CORS preflight considerations and the non-conditional-request semantics. Should OFREP also define an optional custom header form for non-browser clients? -7. **What security requirements should apply to tokenized SSE URLs?** Should OFREP require URL redaction in logs/telemetry, recommend short-lived scoped tokens, and discourage long-term persistence of raw SSE URLs? +1. **Should `refetchEvaluation` be required, or should providers refetch on any SSE message?** + - **Answer:** Requiring a specific `type` field enables future event types without triggering unnecessary refetches. This ADR recommends requiring `type=refetchEvaluation` for forward compatibility. + +2. **Should providers support streaming full evaluation payloads over SSE?** + - **Answer:** The notification-only + re-fetch approach works well for architectures using CDNs and edge workers that can absorb the burst of concurrent re-fetch requests. For providers without that infrastructure, sending the full config or a diff directly over SSE may be more appropriate. Future event types such as `fullConfig` or `patchConfig` could be defined in a follow-up ADR without breaking the existing contract. + +3. **What is the recommended coalescing strategy when multiple SSE connections are specified?** + - **Answer:** Providers should connect to all URLs and coalesce concurrent `refetchEvaluation` events via in-flight deduplication or a short debounce window. Minimum coalescing expectations are left to provider implementations for now. + +4. **Should `inactivityDelaySec` be server-provided or client-side configuration?** + - **Answer:** This ADR specifies it as server-provided with a default of 120 seconds when omitted, allowing the server to tune connection lifecycle. Providers may also expose a client-side override. + +5. **Should non-`refetchEvaluation` SSE messages be forwarded to the provider?** + - **Answer:** A mechanism to forward unknown typed messages to the provider via an events/hook interface could be valuable but is deferred to a future revision. + +6. **Should SSE metadata be transported via query parameters or custom headers?** + - **Answer:** This ADR uses query params (`sseEtag`, `sseLastModified`) as the single transport mechanism for all SDK types. Custom headers were considered but rejected — most SSE client libraries require manual dispatch regardless of event type, and a single mechanism simplifies implementation across browser, mobile, and server environments. CORS preflight costs make custom headers impractical for browser-based SDKs. + +7. **What security requirements should apply to tokenized SSE URLs?** + - **Answer:** Providers must not log or persist SSE URLs as they may contain auth tokens or channel credentials. Further requirements around token lifetime and rotation are left to vendor implementations. ## Implementation Notes From 49dd18b141007c18308aac1e7d0de694c03fdb9a Mon Sep 17 00:00:00 2001 From: Norris Date: Thu, 5 Mar 2026 23:23:34 +0100 Subject: [PATCH 16/26] docs: fix open question answers for inactivityDelaySec precedence and query param rationale Signed-off-by: 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 5ebfdd9..a62d861 100644 --- a/service/adrs/0008-sse-for-bulk-evaluation-changes.md +++ b/service/adrs/0008-sse-for-bulk-evaluation-changes.md @@ -270,13 +270,13 @@ refreshConnection: - **Answer:** Providers should connect to all URLs and coalesce concurrent `refetchEvaluation` events via in-flight deduplication or a short debounce window. Minimum coalescing expectations are left to provider implementations for now. 4. **Should `inactivityDelaySec` be server-provided or client-side configuration?** - - **Answer:** This ADR specifies it as server-provided with a default of 120 seconds when omitted, allowing the server to tune connection lifecycle. Providers may also expose a client-side override. + - **Answer:** This ADR specifies `inactivityDelaySec` as server-provided, defaulting to 120 seconds when omitted. Providers may expose a client-side override, which should take precedence over the server-provided value. 5. **Should non-`refetchEvaluation` SSE messages be forwarded to the provider?** - **Answer:** A mechanism to forward unknown typed messages to the provider via an events/hook interface could be valuable but is deferred to a future revision. 6. **Should SSE metadata be transported via query parameters or custom headers?** - - **Answer:** This ADR uses query params (`sseEtag`, `sseLastModified`) as the single transport mechanism for all SDK types. Custom headers were considered but rejected — most SSE client libraries require manual dispatch regardless of event type, and a single mechanism simplifies implementation across browser, mobile, and server environments. CORS preflight costs make custom headers impractical for browser-based SDKs. + - **Answer:** Query params are used as the single transport mechanism for all SDK types. Custom headers were considered but rejected because non-safelisted headers trigger CORS preflight `OPTIONS` requests in browsers, adding a round-trip on every SSE-triggered re-fetch. Query params also make the SSE origin of the metadata explicit, distinguishing `sseEtag`/`sseLastModified` from standard HTTP conditional request headers (`If-None-Match` / `If-Modified-Since`). 7. **What security requirements should apply to tokenized SSE URLs?** - **Answer:** Providers must not log or persist SSE URLs as they may contain auth tokens or channel credentials. Further requirements around token lifetime and rotation are left to vendor implementations. From 697aa3f18921b28363a0ced97751b4e52d7393a3 Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Thu, 5 Mar 2026 23:26:19 +0100 Subject: [PATCH 17/26] Update service/adrs/0008-sse-for-bulk-evaluation-changes.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> 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 a62d861..295e9ff 100644 --- a/service/adrs/0008-sse-for-bulk-evaluation-changes.md +++ b/service/adrs/0008-sse-for-bulk-evaluation-changes.md @@ -116,7 +116,7 @@ sequenceDiagram Client->>SSE: Connect to SSE URL(s) Note over SSE,Client: Real-time change notification - SSE-->>Client: event: refetchEvaluation (etag, lastModified) + SSE-->>Client: event: message (data.type=refetchEvaluation, etag, lastModified) Client->>Server: POST /ofrep/v1/evaluate/flags?sseEtag=etag&sseLastModified=lastModified alt Flags changed Server-->>Client: 200 OK (new flags + ETag) From bf8c444508f4fa6f4088cc5b4164bcf284641ca4 Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Thu, 5 Mar 2026 23:26:48 +0100 Subject: [PATCH 18/26] Update service/adrs/0008-sse-for-bulk-evaluation-changes.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> 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 295e9ff..c78ef87 100644 --- a/service/adrs/0008-sse-for-bulk-evaluation-changes.md +++ b/service/adrs/0008-sse-for-bulk-evaluation-changes.md @@ -140,7 +140,7 @@ Provider implementation guidelines: 1. After the initial bulk evaluation response, if `refreshConnections` is present, the provider should connect to any entries with a known `type` (currently `"sse"`). 2. On receiving a `refetchEvaluation` event, the provider must re-fetch flag evaluations from the bulk evaluation endpoint. If `etag` is present, it should be sent as `sseEtag` query parameter. If `lastModified` is present, it should be sent as `sseLastModified` query parameter. These query parameters should only be included for requests directly triggered by processing that SSE event. `lastModified` parsing should support Unix timestamp in seconds and date string formats. -3. If `inactivityDelaySec` is specified, the provider should close the SSE connection after the specified inactivity period. On resumption, it must reconnect and immediately perform a full unconditional re-fetch -- without `If-None-Match`, `sseEtag`, or `sseLastModified` -- to ensure the cache reflects the current server state after an unknown period of inactivity. +3. Providers must apply an inactivity timeout for SSE connections using an effective `inactivityDelaySec` value: if `inactivityDelaySec` is specified in the response, use that value; if it is omitted, assume a default of 120 seconds. After this effective inactivity period, the provider should close the SSE connection. On resumption, it must reconnect and immediately perform a full unconditional re-fetch -- without `If-None-Match`, `sseEtag`, or `sseLastModified` -- to ensure the cache reflects the current server state after an unknown period of inactivity. 4. If the SSE connection fails or is unavailable, the provider must fall back to its configured change detection behavior: if polling is enabled, continue with polling; if polling is disabled, continue SSE reconnection attempts and rely on explicit refresh triggers such as `onContextChange`. 5. Providers should implement reconnection with exponential backoff. The native `EventSource` API in browsers handles this automatically. 6. When `onContextChange` is triggered, the provider re-fetches the bulk evaluation without SSE query metadata and updates its connections based on the new response: From e191390841601448d1ba3e3c449bb9d215573661 Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Thu, 5 Mar 2026 23:27:23 +0100 Subject: [PATCH 19/26] Update service/adrs/0008-sse-for-bulk-evaluation-changes.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> 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 c78ef87..fc8d65f 100644 --- a/service/adrs/0008-sse-for-bulk-evaluation-changes.md +++ b/service/adrs/0008-sse-for-bulk-evaluation-changes.md @@ -284,7 +284,7 @@ refreshConnection: ## Implementation Notes - **Provider change detection configuration**: Providers should expose a `changeDetection` configuration option with the following values: - - `sse` *(default)*: Use SSE if the bulk evaluation response includes a `refreshConnections` entry with `type: "sse"`, falling back to polling on connection failure. If no `refreshConnections` are present, polling is used. + - `sse` *(default)*: Use SSE if the bulk evaluation response includes a `refreshConnections` entry with `type: "sse"`. On connection failure, providers MAY fall back to polling only when polling is enabled (for example, when a positive `pollInterval` is configured); otherwise, they SHOULD continue attempting SSE and rely on explicit refresh triggers. If no `refreshConnections` are present, polling is used (subject to the same polling configuration). - `polling`: Ignore `refreshConnections` 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. From efeb9f4b1c764095f6dad2ef8a7957344b694084 Mon Sep 17 00:00:00 2001 From: Norris Date: Sat, 7 Mar 2026 11:05:50 +0100 Subject: [PATCH 20/26] docs: correct CORS rationale for query param transport of SSE metadata Signed-off-by: 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 fc8d65f..4b029fe 100644 --- a/service/adrs/0008-sse-for-bulk-evaluation-changes.md +++ b/service/adrs/0008-sse-for-bulk-evaluation-changes.md @@ -97,7 +97,7 @@ Reconnection and replay behavior: Transporting SSE metadata to the bulk endpoint: - `sseEtag` and `sseLastModified` are SSE-trigger metadata, not standard HTTP conditional request validators for endpoint-level response caching semantics. - `sseEtag` and `sseLastModified` should only be sent when the re-fetch request is directly triggered by a received SSE message. -- For browser-based SDKs, query parameters avoid CORS preflight costs that would be introduced by custom headers. +- For browser-based SDKs, using query parameters instead of custom headers avoids introducing additional non-safelisted headers that would require expanding `Access-Control-Allow-Headers` and helps keep CORS configuration simpler. - The metadata originates from the SSE channel, so query parameters make the source and intent explicit. - This is particularly useful for implementations where the OFREP server validates internal cache state and storage freshness directly (for example, cache + object storage bindings) rather than forwarding conditional headers upstream. - To reduce cross-language date parsing ambiguity, providers and servers should prefer Unix timestamp in seconds for `lastModified` / `sseLastModified` when possible. @@ -276,7 +276,7 @@ refreshConnection: - **Answer:** A mechanism to forward unknown typed messages to the provider via an events/hook interface could be valuable but is deferred to a future revision. 6. **Should SSE metadata be transported via query parameters or custom headers?** - - **Answer:** Query params are used as the single transport mechanism for all SDK types. Custom headers were considered but rejected because non-safelisted headers trigger CORS preflight `OPTIONS` requests in browsers, adding a round-trip on every SSE-triggered re-fetch. Query params also make the SSE origin of the metadata explicit, distinguishing `sseEtag`/`sseLastModified` from standard HTTP conditional request headers (`If-None-Match` / `If-Modified-Since`). + - **Answer:** Query params are used as the single transport mechanism for all SDK types. Custom headers were considered but rejected because non-safelisted headers require expanding `Access-Control-Allow-Headers` in CORS configuration, and introduce additional complexity for browser-based SDKs. Query params also make the SSE origin of the metadata explicit, distinguishing `sseEtag`/`sseLastModified` from standard HTTP conditional request headers (`If-None-Match` / `If-Modified-Since`). 7. **What security requirements should apply to tokenized SSE URLs?** - **Answer:** Providers must not log or persist SSE URLs as they may contain auth tokens or channel credentials. Further requirements around token lifetime and rotation are left to vendor implementations. From d611960002ff647689cad550dd561522f8a98f0f Mon Sep 17 00:00:00 2001 From: Norris Date: Fri, 13 Mar 2026 14:42:42 -0400 Subject: [PATCH 21/26] docs: rename to eventStreams/flagConfigEtag, broaden scope to include server-side providers Signed-off-by: Norris --- .../0008-sse-for-bulk-evaluation-changes.md | 79 +++++++++---------- 1 file changed, 39 insertions(+), 40 deletions(-) diff --git a/service/adrs/0008-sse-for-bulk-evaluation-changes.md b/service/adrs/0008-sse-for-bulk-evaluation-changes.md index 4b029fe..52803ec 100644 --- a/service/adrs/0008-sse-for-bulk-evaluation-changes.md +++ b/service/adrs/0008-sse-for-bulk-evaluation-changes.md @@ -8,9 +8,9 @@ Proposed ## Context -OFREP currently relies exclusively on polling for flag change detection in client-side (static context) providers. 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. +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 focuses primarily on static-context providers (for example web and mobile SDK providers) that use bulk evaluation caching patterns. It does not introduce SSE requirements for dynamic-context providers that primarily use single-flag evaluations. +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. Polling has known limitations: - There is no way to implement real-time flag updates @@ -28,14 +28,13 @@ Server-Sent Events (SSE) is a W3C standard that fits this use case well: ## Decision -Add an optional `refreshConnections` 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. -This is primarily intended for static-context providers (for example web/mobile clients) that rely on bulk evaluations. +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/{flagKey}`). 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 the bulk evaluation via the existing endpoint, rather than streaming full evaluation payloads. This keeps the SSE message format simple, reuses existing infrastructure, and avoids duplicating evaluation logic. +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 `refreshConnections` field to `bulkEvaluationSuccess`: +Add an optional `eventStreams` field to `bulkEvaluationSuccess` and `flagEvaluationSuccess`: ```json { @@ -47,7 +46,7 @@ Add an optional `refreshConnections` field to `bulkEvaluationSuccess`: "variant": "enabled" } ], - "refreshConnections": [ + "eventStreams": [ { "type": "sse", "url": "https://sse.example.com/event-stream?channels=env_abc123_v1", @@ -60,12 +59,12 @@ Add an optional `refreshConnections` field to `bulkEvaluationSuccess`: } ``` -Each refresh connection object has: +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, required): The endpoint URL. The URL is opaque to the provider and 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. - `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`. If omitted, providers should default to `120` seconds. -The `refreshConnections` 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. +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. ### SSE Event Format @@ -83,8 +82,8 @@ 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 `sseEtag` 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 `sseLastModified` query parameter on the re-fetch request. +- `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. SSE envelope fields: - `id` (string, recommended): Event identifier used by SSE clients for resume semantics via `Last-Event-ID`. @@ -95,12 +94,12 @@ Reconnection and replay behavior: - Providers must perform an immediate bulk re-fetch after reconnect, even when replay is supported, to guarantee cache correctness across implementations with different replay retention policies. Transporting SSE metadata to the bulk endpoint: -- `sseEtag` and `sseLastModified` are SSE-trigger metadata, not standard HTTP conditional request validators for endpoint-level response caching semantics. -- `sseEtag` and `sseLastModified` should only be sent when the re-fetch request is directly triggered by a received SSE message. +- `flagConfigEtag` and `flagConfigLastModified` are SSE-trigger metadata, not standard HTTP conditional request validators for endpoint-level response caching semantics. +- `flagConfigEtag` and `flagConfigLastModified` should only be sent when the re-fetch request is directly triggered by a received SSE message. - For browser-based SDKs, using query parameters instead of custom headers avoids introducing additional non-safelisted headers that would require expanding `Access-Control-Allow-Headers` and helps keep CORS configuration simpler. - The metadata originates from the SSE channel, so query parameters make the source and intent explicit. - This is particularly useful for implementations where the OFREP server validates internal cache state and storage freshness directly (for example, cache + object storage bindings) rather than forwarding conditional headers upstream. -- To reduce cross-language date parsing ambiguity, providers and servers should prefer Unix timestamp in seconds for `lastModified` / `sseLastModified` when possible. +- To reduce cross-language date parsing ambiguity, providers and servers should prefer Unix timestamp in seconds for `lastModified` / `flagConfigLastModified` when possible. ### Provider Behavior @@ -111,13 +110,13 @@ sequenceDiagram participant SSE as SSE Endpoint Client->>Server: POST /ofrep/v1/evaluate/flags - Server-->>Client: 200 OK (flags + refreshConnections + ETag header) + Server-->>Client: 200 OK (flags + eventStreams + ETag header) Client->>Client: Cache flags, store ETag Client->>SSE: Connect to SSE URL(s) Note over SSE,Client: Real-time change notification SSE-->>Client: event: message (data.type=refetchEvaluation, etag, lastModified) - Client->>Server: POST /ofrep/v1/evaluate/flags?sseEtag=etag&sseLastModified=lastModified + Client->>Server: POST /ofrep/v1/evaluate/flags?flagConfigEtag=etag&flagConfigLastModified=lastModified alt Flags changed Server-->>Client: 200 OK (new flags + ETag) Client->>Client: Update cache, emit ConfigurationChanged @@ -137,24 +136,24 @@ sequenceDiagram ``` Provider implementation guidelines: -1. After the initial bulk evaluation response, if `refreshConnections` is present, the provider should connect to any entries with a known `type` (currently `"sse"`). -2. On receiving a `refetchEvaluation` event, the provider must re-fetch flag evaluations from the bulk evaluation endpoint. If `etag` is present, it should be sent as `sseEtag` query parameter. If `lastModified` is present, it should be sent as `sseLastModified` query parameter. These query parameters should only be included for requests directly triggered by processing that SSE event. +1. After the initial bulk evaluation response, if `eventStreams` is present, the provider should connect to any entries with a known `type` (currently `"sse"`). +2. On receiving a `refetchEvaluation` event, the provider must re-fetch flag evaluations from the bulk evaluation endpoint. If `etag` is present, it should be sent as `flagConfigEtag` query parameter. If `lastModified` is present, it should be sent as `flagConfigLastModified` query parameter. These query parameters should only be included for requests directly triggered by processing that SSE event. `lastModified` parsing should support Unix timestamp in seconds and date string formats. -3. Providers must apply an inactivity timeout for SSE connections using an effective `inactivityDelaySec` value: if `inactivityDelaySec` is specified in the response, use that value; if it is omitted, assume a default of 120 seconds. After this effective inactivity period, the provider should close the SSE connection. On resumption, it must reconnect and immediately perform a full unconditional re-fetch -- without `If-None-Match`, `sseEtag`, or `sseLastModified` -- to ensure the cache reflects the current server state after an unknown period of inactivity. +3. Providers must apply an inactivity timeout for SSE connections using an effective `inactivityDelaySec` value: if `inactivityDelaySec` is specified in the response, use that value; if it is omitted, assume a default of 120 seconds. After this effective inactivity period, the provider should close the SSE connection. On resumption, it must reconnect and immediately perform a full unconditional re-fetch -- without `If-None-Match`, `flagConfigEtag`, or `flagConfigLastModified` -- to ensure the cache reflects the current server state after an unknown period of inactivity. 4. If the SSE connection fails or is unavailable, the provider must fall back to its configured change detection behavior: if polling is enabled, continue with polling; if polling is disabled, continue SSE reconnection attempts and rely on explicit refresh triggers such as `onContextChange`. 5. Providers should implement reconnection with exponential backoff. The native `EventSource` API in browsers handles this automatically. 6. When `onContextChange` is triggered, the provider re-fetches the bulk evaluation without SSE query metadata and updates its connections based on the new response: - - If `refreshConnections` is absent, close all existing connections and fall back to configured change detection behavior. - - If `refreshConnections` is present and the URL set is unchanged, existing connections may be reused. - - If `refreshConnections` is present and the URL set has changed, close existing connections then connect to the new URLs. + - If `eventStreams` is absent, close all existing connections and fall back to configured change detection behavior. + - If `eventStreams` is present and the URL set is unchanged, existing connections may be reused. + - If `eventStreams` is present and the URL set has changed, close existing connections then connect to the new URLs. 7. Providers SHOULD coalesce concurrent `refetchEvaluation` events into a single re-fetch request (e.g., via in-flight deduplication or a short debounce window) to avoid amplifying load on the flag management system when multiple connections fire simultaneously. ### OpenAPI Schema Additions ```yaml -# Add to /ofrep/v1/evaluate/flags POST parameters: +# Add to /ofrep/v1/evaluate/flags and /ofrep/v1/evaluate/flags/{flagKey} POST parameters: - in: query - name: sseEtag + name: flagConfigEtag description: | Optional SSE-provided ETag metadata for SSE-triggered re-fetches. This is not a standard HTTP conditional request header; it is metadata for server-side @@ -166,7 +165,7 @@ Provider implementation guidelines: example: "\"550e8400-e29b-41d4-a716-446655440000\"" - in: query - name: sseLastModified + 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 / @@ -187,21 +186,20 @@ Provider implementation guidelines: httpDate: value: "Thu, 20 Feb 2026 21:28:18 GMT" -# Add to bulkEvaluationSuccess.properties: -refreshConnections: +# Add to bulkEvaluationSuccess.properties and flagEvaluationSuccess.properties: +eventStreams: type: array description: | - Optional array of real-time change notification connections. This is primarily - intended for static-context providers (for example web/mobile providers) using - bulk evaluation caching patterns. 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. + 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/refreshConnection" + $ref: "#/components/schemas/eventStream" # Add to components.schemas: -refreshConnection: +eventStream: description: | A real-time change notification connection endpoint. The `type` field identifies the push mechanism; currently only `sse` is defined. Providers @@ -246,7 +244,7 @@ refreshConnection: - **Real-time flag updates**: Providers can receive flag change notifications immediately rather than waiting for the next poll interval - **Reduced server load**: Eliminates unnecessary polling requests when flags have not changed - **Vendor-agnostic**: The `url` field is opaque, allowing vendors to use any SSE infrastructure (hosted services like Ably/Pusher, self-hosted endpoints, CDN-based proxies) -- **Backward compatible**: The `refreshConnections` field is fully optional -- servers that don't support it omit the field, providers that don't support it ignore the field and continue their configured change detection behavior +- **Backward compatible**: The `eventStreams` field is fully optional -- servers that don't support it omit the field, providers that don't support it ignore the field and continue their configured change detection behavior - **Builds on existing infrastructure**: Uses the existing bulk evaluation endpoint for data transfer, keeping SSE as a lightweight notification layer ### Negative @@ -276,7 +274,7 @@ refreshConnection: - **Answer:** A mechanism to forward unknown typed messages to the provider via an events/hook interface could be valuable but is deferred to a future revision. 6. **Should SSE metadata be transported via query parameters or custom headers?** - - **Answer:** Query params are used as the single transport mechanism for all SDK types. Custom headers were considered but rejected because non-safelisted headers require expanding `Access-Control-Allow-Headers` in CORS configuration, and introduce additional complexity for browser-based SDKs. Query params also make the SSE origin of the metadata explicit, distinguishing `sseEtag`/`sseLastModified` from standard HTTP conditional request headers (`If-None-Match` / `If-Modified-Since`). + - **Answer:** Query params are used as the single transport mechanism for all SDK types. Custom headers were considered but rejected because non-safelisted headers require expanding `Access-Control-Allow-Headers` in CORS configuration, and introduce additional complexity for browser-based SDKs. Query params also make the SSE origin of the metadata explicit, distinguishing `flagConfigEtag`/`flagConfigLastModified` from standard HTTP conditional request headers (`If-None-Match` / `If-Modified-Since`). 7. **What security requirements should apply to tokenized SSE URLs?** - **Answer:** Providers must not log or persist SSE URLs as they may contain auth tokens or channel credentials. Further requirements around token lifetime and rotation are left to vendor implementations. @@ -284,8 +282,9 @@ refreshConnection: ## Implementation Notes - **Provider change detection configuration**: Providers should expose a `changeDetection` configuration option with the following values: - - `sse` *(default)*: Use SSE if the bulk evaluation response includes a `refreshConnections` entry with `type: "sse"`. On connection failure, providers MAY fall back to polling only when polling is enabled (for example, when a positive `pollInterval` is configured); otherwise, they SHOULD continue attempting SSE and rely on explicit refresh triggers. If no `refreshConnections` are present, polling is used (subject to the same polling configuration). - - `polling`: Ignore `refreshConnections` and rely solely on polling. + - `sse` *(default)*: Use SSE if the bulk evaluation response includes an `eventStreams` entry with `type: "sse"`. On connection failure, providers MAY fall back to polling only when polling is enabled (for example, when a positive `pollInterval` is configured); otherwise, they SHOULD continue attempting SSE and rely on explicit refresh triggers. If no `eventStreams` are present, polling is used (subject to the same polling configuration). + - `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. -- **Static context provider guideline update**: The [static context provider guideline](../../guideline/static-context-provider.md) would need a new section describing SSE connection management alongside the existing polling section. +- **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. +- **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. From a97675bf33dfe0496c54580c5d41832a0c682d27 Mon Sep 17 00:00:00 2001 From: Norris Date: Fri, 13 Mar 2026 14:48:17 -0400 Subject: [PATCH 22/26] docs: clarify SSE data field is a raw string that must be parsed as JSON Signed-off-by: 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 52803ec..4140036 100644 --- a/service/adrs/0008-sse-for-bulk-evaluation-changes.md +++ b/service/adrs/0008-sse-for-bulk-evaluation-changes.md @@ -68,7 +68,7 @@ The `eventStreams` field is an array to support vendors whose infrastructure may ### SSE Event Format -Events use the standard [SSE event format](https://html.spec.whatwg.org/multipage/server-sent-events.html) with a JSON `data` field: +Events use the standard [SSE event format](https://html.spec.whatwg.org/multipage/server-sent-events.html). The SSE `data` field is a raw string (multiple `data:` lines are concatenated per the W3C spec) that providers must parse as JSON: ``` id: evt-1234 From 8f5e3fca0767573941433368bf7d73cec9a69ef7 Mon Sep 17 00:00:00 2001 From: Norris Date: Fri, 13 Mar 2026 14:49:44 -0400 Subject: [PATCH 23/26] docs: add application-level vs implementation-level rationale to query param answer Signed-off-by: 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 4140036..52b5c35 100644 --- a/service/adrs/0008-sse-for-bulk-evaluation-changes.md +++ b/service/adrs/0008-sse-for-bulk-evaluation-changes.md @@ -274,7 +274,7 @@ eventStream: - **Answer:** A mechanism to forward unknown typed messages to the provider via an events/hook interface could be valuable but is deferred to a future revision. 6. **Should SSE metadata be transported via query parameters or custom headers?** - - **Answer:** Query params are used as the single transport mechanism for all SDK types. Custom headers were considered but rejected because non-safelisted headers require expanding `Access-Control-Allow-Headers` in CORS configuration, and introduce additional complexity for browser-based SDKs. Query params also make the SSE origin of the metadata explicit, distinguishing `flagConfigEtag`/`flagConfigLastModified` from standard HTTP conditional request headers (`If-None-Match` / `If-Modified-Since`). + - **Answer:** Query params are used as the single transport mechanism for all SDK types. Custom headers were considered but rejected because non-safelisted headers require expanding `Access-Control-Allow-Headers` in CORS configuration, and introduce additional complexity for browser-based SDKs. Query params also make the SSE origin of the metadata explicit, distinguishing `flagConfigEtag`/`flagConfigLastModified` from standard HTTP conditional request headers (`If-None-Match` / `If-Modified-Since`). This aligns with the existing OFREP convention where application-level data (flag keys, evaluation context, SSE metadata) is transported in path, query, and body parameters, while implementation concerns like caching are handled via HTTP headers. 7. **What security requirements should apply to tokenized SSE URLs?** - **Answer:** Providers must not log or persist SSE URLs as they may contain auth tokens or channel credentials. Further requirements around token lifetime and rotation are left to vendor implementations. From 7022fa9aa0b4f3c0c083c47feaa7b8c009fefd80 Mon Sep 17 00:00:00 2001 From: Norris Date: Fri, 13 Mar 2026 15:08:25 -0400 Subject: [PATCH 24/26] docs: clarify SSE provider semantics and timeout precedence Signed-off-by: Norris --- .../adrs/0008-sse-for-bulk-evaluation-changes.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/service/adrs/0008-sse-for-bulk-evaluation-changes.md b/service/adrs/0008-sse-for-bulk-evaluation-changes.md index 52b5c35..7f8cf66 100644 --- a/service/adrs/0008-sse-for-bulk-evaluation-changes.md +++ b/service/adrs/0008-sse-for-bulk-evaluation-changes.md @@ -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/{flagKey}`). 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`) 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. 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 `flagEvaluationSuccess`: +Add an optional `eventStreams` field to `bulkEvaluationSuccess` and `serverEvaluationSuccess`: ```json { @@ -62,7 +62,7 @@ Add an optional `eventStreams` field to `bulkEvaluationSuccess` and `flagEvaluat 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, required): The endpoint URL. The URL is opaque to the provider and 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. -- `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`. If omitted, providers should default to `120` seconds. +- `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. 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. @@ -85,6 +85,8 @@ Event data fields: - `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. +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. + SSE envelope fields: - `id` (string, recommended): Event identifier used by SSE clients for resume semantics via `Last-Event-ID`. @@ -139,7 +141,7 @@ Provider implementation guidelines: 1. After the initial bulk evaluation response, if `eventStreams` is present, the provider should connect to any entries with a known `type` (currently `"sse"`). 2. On receiving a `refetchEvaluation` event, the provider must re-fetch flag evaluations from the bulk evaluation endpoint. If `etag` is present, it should be sent as `flagConfigEtag` query parameter. If `lastModified` is present, it should be sent as `flagConfigLastModified` query parameter. These query parameters should only be included for requests directly triggered by processing that SSE event. `lastModified` parsing should support Unix timestamp in seconds and date string formats. -3. Providers must apply an inactivity timeout for SSE connections using an effective `inactivityDelaySec` value: if `inactivityDelaySec` is specified in the response, use that value; if it is omitted, assume a default of 120 seconds. After this effective inactivity period, the provider should close the SSE connection. On resumption, it must reconnect and immediately perform a full unconditional re-fetch -- without `If-None-Match`, `flagConfigEtag`, or `flagConfigLastModified` -- to ensure the cache reflects the current server state after an unknown period of inactivity. +3. Providers must apply an inactivity timeout for SSE connections using an effective `inactivityDelaySec` value determined as follows: if a client-side override is configured, use that value; otherwise, if `inactivityDelaySec` is specified in the response, use that value; otherwise, assume a default of 120 seconds. After this effective inactivity period, the provider should close the SSE connection. On resumption, it must reconnect and immediately perform a full unconditional re-fetch -- without `If-None-Match`, `flagConfigEtag`, or `flagConfigLastModified` -- to ensure the cache reflects the current server state after an unknown period of inactivity. 4. If the SSE connection fails or is unavailable, the provider must fall back to its configured change detection behavior: if polling is enabled, continue with polling; if polling is disabled, continue SSE reconnection attempts and rely on explicit refresh triggers such as `onContextChange`. 5. Providers should implement reconnection with exponential backoff. The native `EventSource` API in browsers handles this automatically. 6. When `onContextChange` is triggered, the provider re-fetches the bulk evaluation without SSE query metadata and updates its connections based on the new response: @@ -151,7 +153,7 @@ Provider implementation guidelines: ### OpenAPI Schema Additions ```yaml -# Add to /ofrep/v1/evaluate/flags and /ofrep/v1/evaluate/flags/{flagKey} POST parameters: +# Add to /ofrep/v1/evaluate/flags and /ofrep/v1/evaluate/flags/{key} POST parameters: - in: query name: flagConfigEtag description: | @@ -186,7 +188,7 @@ Provider implementation guidelines: httpDate: value: "Thu, 20 Feb 2026 21:28:18 GMT" -# Add to bulkEvaluationSuccess.properties and flagEvaluationSuccess.properties: +# Add to bulkEvaluationSuccess.properties and serverEvaluationSuccess.properties: eventStreams: type: array description: | @@ -268,7 +270,7 @@ eventStream: - **Answer:** Providers should connect to all URLs and coalesce concurrent `refetchEvaluation` events via in-flight deduplication or a short debounce window. Minimum coalescing expectations are left to provider implementations for now. 4. **Should `inactivityDelaySec` be server-provided or client-side configuration?** - - **Answer:** This ADR specifies `inactivityDelaySec` as server-provided, defaulting to 120 seconds when omitted. Providers may expose a client-side override, which should take precedence over the server-provided value. + - **Answer:** This ADR allows both. Providers use a client-side override when configured; otherwise they use the server-provided `inactivityDelaySec`; otherwise they default to 120 seconds. 5. **Should non-`refetchEvaluation` SSE messages be forwarded to the provider?** - **Answer:** A mechanism to forward unknown typed messages to the provider via an events/hook interface could be valuable but is deferred to a future revision. From c52cda48b8a1ddef3bdf2aec8fe60f5a6a5d0b39 Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Mon, 16 Mar 2026 14:20:33 -0400 Subject: [PATCH 25/26] docs: add structured SSE endpoint option Signed-off-by: Jonathan Norris --- .../0008-sse-for-bulk-evaluation-changes.md | 32 +++++++++++++++++-- 1 file changed, 30 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 7f8cf66..a6aca3f 100644 --- a/service/adrs/0008-sse-for-bulk-evaluation-changes.md +++ b/service/adrs/0008-sse-for-bulk-evaluation-changes.md @@ -61,9 +61,12 @@ 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, required): The endpoint URL. The URL is opaque to the provider and 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. +- `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. - `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`. + 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. ### SSE Event Format @@ -209,7 +212,11 @@ eventStream: type: object required: - type - - url + oneOf: + - required: + - url + - required: + - endpoint properties: type: type: string @@ -227,6 +234,27 @@ eventStream: channel identifiers, or other query parameters as needed by the vendor's infrastructure. example: "https://sse.example.com/event-stream?channels=env_abc123_v1" + 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`. + 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" inactivityDelaySec: type: integer minimum: 1 From 8740404f479fc070f4c351753408d56234679f51 Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Mon, 16 Mar 2026 14:21:35 -0400 Subject: [PATCH 26/26] docs: enforce exclusive SSE endpoint fields Signed-off-by: Jonathan Norris --- service/adrs/0008-sse-for-bulk-evaluation-changes.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/service/adrs/0008-sse-for-bulk-evaluation-changes.md b/service/adrs/0008-sse-for-bulk-evaluation-changes.md index a6aca3f..112d62a 100644 --- a/service/adrs/0008-sse-for-bulk-evaluation-changes.md +++ b/service/adrs/0008-sse-for-bulk-evaluation-changes.md @@ -215,8 +215,14 @@ eventStream: oneOf: - required: - url + not: + required: + - endpoint - required: - endpoint + not: + required: + - url properties: type: type: string