FlagrshipGuidesReference
esc

Concepts

The five ideas the whole system is built on. Read this once and the rest of the docs are reference.

Flags, environments, and configs

A flag is a named switch, identified by a key like new-checkout. It exists once per organization. An environment is an isolated namespace: production, staging, development. A flag has one config per environment, and that config is where the state lives: enabled or not, rollout percentage, lock status.

Creating a flag creates a config in every environment at once, disabled and at 0%. Enabling it in staging does nothing to production. The same flag, different state per environment.

The environment comes from the key

An API key is scoped to exactly one environment. There is no ?env= parameter anywhere in the API. When you call with a staging key, you get staging. This is what makes a leaked staging key harmless to production, and it is why the CLI stores one key per environment and --env picks a key rather than sending a parameter.

PrefixScopeTypical use
sk_read_readSDKs. Can fetch config, cannot change anything.
sk_test_read, writeCLI in non-production environments.
sk_live_read, writeCLI in production.
sk_admin_read, write, adminLock, unlock, archive, key management, CI pipelines.

Deterministic bucketing

A percentage rollout does not pick users at random. Each user is placed in one of 100 buckets by hashing the flag key and the user ID together. A user is on when their bucket number is below the rollout percentage.

bucket = murmurhash3_x86_32("new-checkout:alice", seed = 0) % 100 // 40
on = bucket < rolloutPercentage // 40 < 25 → false
// 40 < 50 → true

Three things follow from this, and all of them matter:

  • Users never flicker. Alice is bucket 40 for new-checkout on every server, in every language, forever.
  • Raising the percentage only adds users. Going from 25% to 50% keeps everyone who was in and adds buckets 25 through 49. Nobody who saw the feature loses it.
  • Different flags get different cohorts. The flag key is part of the hash, so Alice is bucket 40 for one flag and bucket 95 for another. Rolling out two flags at 25% does not give the same quarter of users both.

Because the hash runs inside the SDK, the API never sees a user ID. There is no endpoint that accepts one. Nothing to store, nothing to leak.

Rollback is an undo

rollback restores the state from before the most recent change to that flag in that environment, and records itself as a change. So a second rollback undoes the first. It is a one-deep undo stack, not a history walk and not a return to some blessed state.

$ flagrship rollout new-checkout 100 # 0 → 100
$ flagrship rollout new-checkout 50 # 100 → 50
$ flagrship rollback new-checkout # 50 → 100 (undo the last change)
$ flagrship rollback new-checkout # 100 → 50 (undo the undo)

Lock and unlock are not part of the stack. They are deliberate admin decisions, and an undo should never quietly re-lock a flag someone chose to unlock.

Lock

A locked config rejects enable, disable, rollout, and rollback with a 409 that includes the reason. Locking requires an admin key and a reason; the reason is what everyone who bumps into the lock will read. Use it during incidents: the vulnerable code stays deployed but dormant, and nobody can turn it back on by accident until an admin unlocks it.

Archiving a flag is refused while it is locked in any environment. Archiving evaluates to false everywhere, which is exactly what a lock exists to prevent.

Everything is audited

Every change writes an audit row in the same database transaction as the change itself. Who, when, before, after, and how (API key prefix, IP, request ID). If the audit write fails, the change does not happen. There is no code path that mutates a flag without leaving a row behind.

Audit history is append-only. Revoking an API key does not delete it either; a revoked key is evidence of what happened during an incident.

What happens when the API is unreachable

The SDK fetches config at boot and polls afterwards. If the first fetch fails, ready() still resolves, and every flag evaluates to the default you wrote in code. If a later poll fails, the SDK keeps serving the last config it received. Evaluation is always in memory. Your application never blocks on Flagrship and never makes a network call per check.

Polls carry an If-None-Match header with the last ETag. When nothing has changed, the API answers304 with an empty body. Config crosses the wire once, then only when you change it.

Flagrship · Ship without a release.