The Launch SDK contract
Every e-volv Launch SDK implements this contract, and every SDK is released only after two conformance suites have checked it: one for evaluation, one for delivery. If you are writing a client for a language we do not ship, this page and the HTTP API reference are the whole specification. The SDKs index lists which platforms are available today.
Two kinds of SDK
| Kind | Platforms | Key | Receives | Evaluates |
|---|---|---|---|---|
| Server | Node.js, edge runtimes, Python, Go, Ruby, Rust, JVM server | evk_… | The environment’s full ruleset | Locally, with a port of the evaluation kernel |
| Client | Browser, iOS and macOS, Android, React Native, Flutter | evk_pub_… | Evaluated values for one context | Never — reads the values it was sent |
A client SDK never contains the evaluation kernel and never requests the ruleset. A server SDK never sends Origin or x-evolve-app-id. The control plane refuses both mistakes; an SDK must not provoke them. The keys page explains which key belongs where.
Wire
The base URL is the origin of the SDK’s Observer url option plus /api/public/v1/flags, overridable with flags.url. With neither set it is https://api.e-volv.io/api/public/v1/flags. Every request carries:
Authorization: Bearer <key>.- For client keys,
Origin(browsers send the page’s own) or, on native platforms,x-evolve-app-id: <appId>. User-Agent: e-volv-<sdk>/<version>. Browsers cannot set a user agent, so they sendx-evolve-sdkinstead.
GET /bootstrap?v=2 — server ruleset
- Send
If-None-Match: "<etag>"when a ruleset is held.200returns the ruleset (rulesetVersion,environmentId,etag,flags,segments) with anETagheader;304means keep the held one. - An SDK accepts any
rulesetVersionup to 2 and treats a higher one as unreadable: it keeps the held ruleset and logs once.
GET /bootstrap?context=<json> — client values
- The context is URL-encoded compact JSON of at most 4,096 characters before encoding. The server treats a larger or malformed context as empty, so the SDK drops attributes — never the key or kind — to fit, and logs once.
200returnsflags: { [key]: { value, variant, reason } }for the flags marked available to client keys. A flag that is absent evaluates to the caller’s default with reasonFLAG_NOT_FOUND.
GET /stream — change notifications (server SDKs)
event: ruleset
data: {"environmentId":"…","etag":"…"}
event: ruleset
data: {"environmentId":"…","changedAt":1757775600000}
event: heartbeat
data: {}- On connect the stream names the current etag; fetch the ruleset if it differs from the one held. On a change, always fetch (with
If-None-Match). - A heartbeat arrives every 25 seconds. No event for 60 seconds means the connection is dead: close it and reconnect.
429withRetry-Aftermeans the workspace is at its streaming cap: poll, and retry the stream after Retry-After (at least 5 minutes). Client SDKs never stream; they poll, and a client key presented here is refused with403.
POST /exposures
The body is { "exposures": [...] }, at most 1,000 per request. Each exposure carries a unique id (a UUID), ts (ISO-8601 UTC), flagKey, reason, and optionally variant, subject, contextKind, attributes, count and sampleRate. The response is always 202 { accepted }; accepted: 0 means dropped, not failed. The endpoint is idempotent on (ts, id), so a retried batch never counts twice.
GET /ping
Returns the environment, the key kind (server or client), the flag count and the etag. It backs the install check and is never called on a hot path.
Status codes
| Response | SDK behaviour |
|---|---|
401 | Key invalid or revoked. Keep serving held values or defaults, log once, retry bootstrap every 5 minutes. |
403 — lacks the scope flags:read | Disable flags with one warning; telemetry is unaffected. No retry until the SDK is initialised again. |
403 — origin or app id refused | Client SDKs log once, naming the origin or app id and the environment’s Access action, and keep serving defaults. |
403 — client key on the stream | Client keys may not open the stream. Client SDKs poll evaluated values instead; this is not retried. |
404 | Launch is not enabled for the workspace, or the key is not bound to an environment. Handled as 401. |
429 | Honour Retry-After (seconds). The delivery rate limit is 3,000 requests per minute per key. |
5xx, timeout, network error | Retry with backoff and keep serving held values. |
Public API
Names are fixed across languages, so docs, snippets and support read the same everywhere; only the spelling follows each language’s idiom (Bool in Go, last_updated_at in Python and Ruby). The flags handle is reached from the Observer client the SDK already exposes. Server SDKs take the context on every call; client SDKs hold one current context set by identify.
| Operation | Behaviour |
|---|---|
| Typed read | bool, string, number, json — synchronous, never throws, never performs I/O |
| Detail | detail(key, default) returns value, variant and reason |
| Ready | ready(timeout) — the only call that waits, and only when the caller chooses to |
| Identify (client SDKs) | identify(context) replaces the one current context and fetches its values |
| Change listener | onChange(fn), returning a way to unsubscribe |
| Staleness | lastUpdatedAt — when the held payload was last confirmed |
| Install check | verify() — calls GET /ping, never on a hot path |
| Close | Through the Observer client’s shutdown or close |
A flag whose served value is not of the requested type returns the caller’s default with reason TYPE_MISMATCH.
Configuration
These options are added to each SDK’s existing init options. The spelling follows the language; the meaning and defaults do not change.
| Option | Server | Client | Meaning |
|---|---|---|---|
flags.enabled | true | true | Start the flags client. |
flags.mode | stream | poll | stream, poll or offline. Offline never contacts the control plane: a bootstrap snapshot or defaults only. |
flags.pollIntervalSeconds | 30 | 60 | Poll period; the minimum is 15. |
flags.cache | platform temp or state directory | platform storage | Where the last-known payload is kept; false disables it. |
flags.bootstrap | none | none | A bundled ruleset (server) or values map (client) served before the first fetch. |
flags.context | — | {} | Client only: the initial context. |
flags.exposures.enabled | true | true | Record exposures. |
flags.exposures.sampleRate | 1.0 | 1.0 | Probability an evaluation is recorded; sent as sampleRate. |
flags.exposures.dedupeWindowSeconds | 60 | 300 | Suppress repeats of the same flag, variant, context kind and subject. |
flags.exposures.sendAttributes | false | false | Send context attributes with exposures; private attributes are always removed. |
flags.privateAttributes | [] | [] | Attribute names never sent anywhere. |
Retry and backoff
Reconnects and retries use full-jitter exponential backoff with a base of 1 second and a cap of 60 seconds:
sleep = random(0, min(60, 2 ** attempt)) // seconds; base 1 s, cap 60 s
After two consecutive stream failures a server SDK switches to polling, attempts the stream again every 5 minutes while polling, and returns to streaming when that succeeds.
Offline behaviour: the degradation ladder
An SDK never lets the control plane break an evaluation. Each row below is a scenario in the delivery conformance suite.
| # | Situation | Evaluation returns | SDK does |
|---|---|---|---|
| L1 | Payload held and fresh | Evaluated value | Normal operation |
| L2 | Control plane returns 5xx or times out | Last held value | Back off; log once per outage |
| L3 | Stream severed | Last held value | Reconnect with backoff; fetch the ruleset again on reconnect |
| L4 | Stream fails twice in a row | Last held value | Poll every pollIntervalSeconds; retry the stream every 5 minutes |
| L5 | Nothing held yet; a valid cache for this environment exists | Cached value | Serve the cache, fetch in the background, swap without a flicker for unchanged flags |
| L6 | Nothing held; no valid cache; no bootstrap snapshot | Caller’s default, reason FLAG_NOT_FOUND | Log “flags not ready” once, on the first evaluation |
| L7 | Cache corrupt, unreadable, or for another environment | As L6 | Ignore it and overwrite silently |
| L8 | Payload with an unknown rulesetVersion | Last held value | Ignore the payload; log once |
| L9 | Payload older than 5 minutes while the connection reports healthy | Held value | Log a staleness warning once; lastUpdatedAt shows the age |
| L10 | Flag absent from the payload | Caller’s default, reason FLAG_NOT_FOUND | Nothing |
| L11 | Key lacks flags:read, or 401 or 404 | Held value, or as L6 | See status codes above |
Evaluation never blocks. ready() is the only call that waits, and only when the caller chose to call it.
Cache
One file, or one storage entry, per environment, written atomically (write a temporary file, then rename; in a browser, one localStorage key):
{
"formatVersion": 1,
"kind": "ruleset",
"environmentId": "flag_environment:e1",
"rulesetVersion": 2,
"etag": "3f2a…",
"savedAt": "2026-09-13T15:00:00.000Z",
"payload": {}
}- Server SDKs store
kind: "ruleset"in a file namedevolve-flags-<first 12 hex of sha1(key)>.json, so two keys on one host never share a file and the key itself is never written to disk. - Client SDKs store
kind: "values"with a sha1 fingerprint of the context. A cache written for a different context is served only until the first fetch for the new context completes. - A reader rejects an unknown
formatVersion, arulesetVersionabove 2, unparseable JSON and any file over 10 MB. A structurally valid cache is served at cold start; if the first fetch returns a different environment, the SDK drops the cached payload and overwrites the file.
Evaluation semantics
Server SDKs port one evaluation kernel rather than reimplementing it, so a flag resolves identically in every language. Precedence, in order:
- A killed flag serves its kill variant.
- Prerequisites: a gate flag must be serving the required variant.
- Individual targets.
- Rules, in order — the first match wins. Clauses match on attributes, with dotted or
/paths, and on segments. - Percentage rollout.
- The default variant.
- Rollouts bucket a subject deterministically:text
bucket(flagKey, subject, salt) = parseInt(sha1_hex(salt ? flagKey + ":" + salt + ":" + subject : flagKey + ":" + subject).slice(0, 8), 16) % 10000 - The subject is the context’s
keyfor the evaluated kind, falling back totargetingKey. Multi-kind contexts are supported. - When operand types differ, values compare as strings, so
42equals"42". - Regular expressions are compiled once and cached; a pattern that fails to compile never matches, and an attribute value over 1,024 characters never matches.
- Segments marked external cannot be resolved locally; a server SDK treats membership in one as false.
- Evaluation returns value, variant and reason and never throws; an internal failure returns the fallback with reason
ERROR.
Exposures
- An exposure is recorded on each evaluation of a flag present in the payload — never for
FLAG_NOT_FOUND,TYPE_MISMATCHorERROR— after sampling and de-duplication. - De-duplication keys on flag, variant, context kind and subject within the dedupe window. Suppressed repeats are counted and sent as
counton the next exposure for that key. - Exposures travel through the SDK’s existing Observer HTTP client and retry policy, batched at up to 1,000 or every flush interval, whichever comes first. The buffer holds 10,000; the oldest are dropped first.
- Private attributes are removed before anything is sent. Disabling telemetry disables exposures and leaves evaluation working.
Conformance and release
Server SDKs run a shared evaluation fixture — every case compares value, variant and reason, plus the bucketing vectors. Every SDK runs the delivery suite: a driver starts a stub control plane, drives the SDK over a line protocol, reconfigures the stub between steps (errors, hangs, dropped streams, changed rulesets, corrupted caches) and judges both the answers and what the stub recorded. Every evaluation must answer within 5 milliseconds in every scenario, including a hung control plane.
Flags ship as a minor release of each Observer SDK that was already published, and in the first release of the others. An SDK is marked available — and its docs page and in-app snippet go live — only after its registry release exists and both suites are green for it.