diff --git a/service/adrs/0009-local-storage-for-static-context-providers.md b/service/adrs/0009-local-storage-for-static-context-providers.md index 3b45619..f3cb9a6 100644 --- a/service/adrs/0009-local-storage-for-static-context-providers.md +++ b/service/adrs/0009-local-storage-for-static-context-providers.md @@ -8,6 +8,8 @@ Accepted Proposed amendment (2026-06-19): tie the cache key to the OFREP resource the evaluation was fetched from by including the provider's bound `domain`, the OFREP base URL, and the auth credential, in addition to the `targetingKey`, and expose a cache-key generator function so applications can customize the key. See [open-feature/spec#393](https://github.com/open-feature/spec/pull/393). +Proposed amendment (2026-09-29): do not persist evaluations for contexts with no `targetingKey`, since the cache key would then carry no identity and the entry could be served to a different subject. Applications that want persistence for anonymous contexts opt in with a cache-key generator. + ## Context OFREP static-context providers evaluate all flags in one request and then serve evaluations from a local cache. @@ -237,6 +239,18 @@ In `network-first` mode, fallback to a persisted entry is limited to network err When the provider has already initialized from cache (cache hit path in `local-cache-first` mode), authorization or configuration errors from the background refresh should be logged and emitted as `PROVIDER_ERROR` events. The provider should continue serving cached values for the current session rather than revoking a working state. Auth or config errors alone should not invalidate the persisted entry; the cache TTL is what governs when it stops being served. This avoids degrading subsequent cold starts to defaults while the error is investigated. +### Missing targeting key + +The `targetingKey` is optional, so a provider may be asked to evaluate a context without one. Providers must still evaluate normally and must not raise `TARGETING_KEY_MISSING`, but they should not persist the result. + +Without a `targetingKey` the cache key carries no identity, so it reduces to the OFREP resource inputs (base URL, auth credential, and bound `domain`) and is identical for every context against that resource. Nothing invalidates the entry when the subject or the rest of the context changes. An application that signs a user out and continues anonymously, or that changes anonymous properties such as `country` or `plan`, would be served the previous evaluation even though the server would return different values. + +Providers should therefore treat a context with no `targetingKey` as non-persistable, neither reading nor writing an entry, so `local-cache-first` behaves like `disabled` for that context. A provider holding an entry from an earlier context that had a `targetingKey` should clear it when the `targetingKey` is removed. + +Applications that want persistence for anonymous contexts should supply a cache-key generator. Configuring one asserts that the returned key material identifies the subject, for example a stable device or anonymous ID, and the provider persists using that key. + +Entries are cleared based on the derived cache key, not the `targetingKey` alone. A generator that includes other context properties can change the key while the `targetingKey` is unchanged, leaving a stale entry behind. + ### Refresh and revalidation When connectivity returns or during normal polling, the provider should resume its normal refresh behavior. @@ -310,7 +324,8 @@ A single default (local-cache-first) with an explicit per-application opt-out is - Providers should avoid persisting raw `targetingKey` values when `cacheKeyHash` is sufficient for matching - Providers should expose a `cacheMode` option with values `local-cache-first` (default), `network-first`, and `disabled`. `network-first` and `disabled` block `initialize()` on the network request; `local-cache-first` returns from `initialize()` immediately when a persisted entry exists - Providers should expose an optional cache-key generator function so applications and wrapping providers can customize the key material (narrowing or broadening the default); the provider always hashes whatever the generator returns -- Providers should clear or replace persisted entries when the cache key changes, such as on logout or user switch (`targetingKey` change) or when the provider is re-bound to a different `domain` +- Providers should not read or write a persisted entry for a context with no `targetingKey` unless a cache-key generator is configured; `local-cache-first` behaves like `disabled` for such contexts +- Providers should clear or replace persisted entries when the derived cache key changes, such as on logout or user switch (a `targetingKey` change, including to or from absent) or when the provider is re-bound to a different `domain`. Compare the key material the generator produces, not the `targetingKey` alone - In `local-cache-first` mode, the `initialize()` function should return immediately when a matching cached entry exists, allowing the SDK to emit `PROVIDER_READY` from cache - Providers should emit `PROVIDER_CONFIGURATION_CHANGED` when fresh values replace cached values after a background refresh - If `onContextChanged()` is called while a background refresh is still in-flight, the provider should cancel or discard the in-flight request. The context-change evaluation supersedes it and should be the authoritative write to the persisted entry