e-volv
Docs menu

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

KindPlatformsKeyReceivesEvaluates
ServerNode.js, edge runtimes, Python, Go, Ruby, Rust, JVM serverevk_…The environment’s full rulesetLocally, with a port of the evaluation kernel
ClientBrowser, iOS and macOS, Android, React Native, Flutterevk_pub_…Evaluated values for one contextNever — 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 send x-evolve-sdk instead.

GET /bootstrap?v=2 — server ruleset

  • Send If-None-Match: "<etag>" when a ruleset is held. 200 returns the ruleset (rulesetVersion, environmentId, etag, flags, segments) with an ETag header; 304 means keep the held one.
  • An SDK accepts any rulesetVersion up 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.
  • 200 returns flags: { [key]: { value, variant, reason } } for the flags marked available to client keys. A flag that is absent evaluates to the caller’s default with reason FLAG_NOT_FOUND.

GET /stream — change notifications (server SDKs)

text
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.
  • 429 with Retry-After means 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 with 403.

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

ResponseSDK behaviour
401Key invalid or revoked. Keep serving held values or defaults, log once, retry bootstrap every 5 minutes.
403 — lacks the scope flags:readDisable flags with one warning; telemetry is unaffected. No retry until the SDK is initialised again.
403 — origin or app id refusedClient SDKs log once, naming the origin or app id and the environment’s Access action, and keep serving defaults.
403 — client key on the streamClient keys may not open the stream. Client SDKs poll evaluated values instead; this is not retried.
404Launch is not enabled for the workspace, or the key is not bound to an environment. Handled as 401.
429Honour Retry-After (seconds). The delivery rate limit is 3,000 requests per minute per key.
5xx, timeout, network errorRetry 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.

OperationBehaviour
Typed readbool, string, number, json — synchronous, never throws, never performs I/O
Detaildetail(key, default) returns value, variant and reason
Readyready(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 listeneronChange(fn), returning a way to unsubscribe
StalenesslastUpdatedAt — when the held payload was last confirmed
Install checkverify() — calls GET /ping, never on a hot path
CloseThrough 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.

OptionServerClientMeaning
flags.enabledtruetrueStart the flags client.
flags.modestreampollstream, poll or offline. Offline never contacts the control plane: a bootstrap snapshot or defaults only.
flags.pollIntervalSeconds3060Poll period; the minimum is 15.
flags.cacheplatform temp or state directoryplatform storageWhere the last-known payload is kept; false disables it.
flags.bootstrapnonenoneA bundled ruleset (server) or values map (client) served before the first fetch.
flags.context{}Client only: the initial context.
flags.exposures.enabledtruetrueRecord exposures.
flags.exposures.sampleRate1.01.0Probability an evaluation is recorded; sent as sampleRate.
flags.exposures.dedupeWindowSeconds60300Suppress repeats of the same flag, variant, context kind and subject.
flags.exposures.sendAttributesfalsefalseSend 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:

text
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.

#SituationEvaluation returnsSDK does
L1Payload held and freshEvaluated valueNormal operation
L2Control plane returns 5xx or times outLast held valueBack off; log once per outage
L3Stream severedLast held valueReconnect with backoff; fetch the ruleset again on reconnect
L4Stream fails twice in a rowLast held valuePoll every pollIntervalSeconds; retry the stream every 5 minutes
L5Nothing held yet; a valid cache for this environment existsCached valueServe the cache, fetch in the background, swap without a flicker for unchanged flags
L6Nothing held; no valid cache; no bootstrap snapshotCaller’s default, reason FLAG_NOT_FOUNDLog “flags not ready” once, on the first evaluation
L7Cache corrupt, unreadable, or for another environmentAs L6Ignore it and overwrite silently
L8Payload with an unknown rulesetVersionLast held valueIgnore the payload; log once
L9Payload older than 5 minutes while the connection reports healthyHeld valueLog a staleness warning once; lastUpdatedAt shows the age
L10Flag absent from the payloadCaller’s default, reason FLAG_NOT_FOUNDNothing
L11Key lacks flags:read, or 401 or 404Held value, or as L6See 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):

json
{
  "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 named evolve-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, a rulesetVersion above 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:

  1. A killed flag serves its kill variant.
  2. Prerequisites: a gate flag must be serving the required variant.
  3. Individual targets.
  4. Rules, in order — the first match wins. Clauses match on attributes, with dotted or / paths, and on segments.
  5. Percentage rollout.
  6. 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 key for the evaluated kind, falling back to targetingKey. Multi-kind contexts are supported.
  • When operand types differ, values compare as strings, so 42 equals "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_MISMATCH or ERROR — 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 count on 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.