Feature flags
Toggle behavior in a running app without a redeploy. Package: internal/api/feature_flags.go, internal/store/feature_flag.go.
Why this isn't an env var
Env vars are baked into a container at create time and are not live-updatable: changing one requires a redeploy or restart. Feature flags toggle behavior without either. Instead, your app calls the control plane's HTTP API at runtime to read the current flag value, the same model as external services like LaunchDarkly or Unleash, but self-hosted.
Authentication reuses the existing API token system. Create a token scoped to the read ability, inject it as a { secret: true } env var, and have your app send it as a bearer token to the evaluate endpoint. No new auth surface needed.
Scope and key uniqueness
A flag is owned by one app (using the same service_name scoping as scheduled-tasks and alerts). The dashboard tabs and CLI commands are scoped by app.
The flag's key (the string your app uses to look it up) is globally unique across the entire control plane, not just within that app. The evaluate endpoint is deliberately flat (GET /api/v1/flags/evaluate/{key}, no app name in the URL) because API tokens carry no app scoping.
Rollout percentage
enabled is a hard kill switch: false disables the flag for all callers.
When enabled is true, rollout_percentage (0-100) buckets callers using consistent hashing (FNV-1a). The same identifier always lands on the same side of a partial rollout, never a fresh coin flip on each call.
Pass a stable per-user or per-device value as the identifier query parameter for per-user rollouts. Omit it and all callers without an identifier share the same outcome.
Integration model
Create a flag (dashboard: an app's "Feature flags" tab, or the CLI):
bashlevelrail-cli flags create my-app --key new-checkout --name "New checkout" --rollout 25Create a read-scoped API token (Settings -> Tokens, or
levelrail-cli tokens create --abilities read), then inject it into your app as a secret env var, e.g.FLAGS_TOKEN.Call the evaluate endpoint from your app's own code:
bashcurl -s \ -H "Authorization: Bearer $FLAGS_TOKEN" \ "https://your-control-plane/api/v1/flags/evaluate/new-checkout?identifier=user-123"Response:
json{ "key": "new-checkout", "enabled": true }That's the entire contract: no SDK exists yet (see below), so any language can call this with a plain HTTP client.
Toggle it live: flip the enabled switch or adjust the rollout slider in the dashboard (or
levelrail-cli flags set), and the very next call to the evaluate endpoint reflects it, with no redeploy or restart of your app.
API reference
| Method | Path | Ability |
|---|---|---|
POST | /api/v1/apps/{name}/flags | write |
GET | /api/v1/apps/{name}/flags | read |
GET | /api/v1/apps/{name}/flags/{id} | read |
PUT | /api/v1/apps/{name}/flags/{id} | write |
DELETE | /api/v1/apps/{name}/flags/{id} | write |
GET | /api/v1/flags/evaluate/{key}?identifier=... | read |
The evaluate response is deliberately minimal (just key and enabled) because apps may call it on every request.
CLI
levelrail-cli flags create <app> --key KEY --name NAME [--description DESC] [--disabled] [--rollout PERCENT]
levelrail-cli flags list <app>
levelrail-cli flags get <app> <id>
levelrail-cli flags set <app> <id> --name NAME [--description DESC] [--disabled] [--rollout PERCENT]
levelrail-cli flags delete <app> <id>Not built yet (deliberate follow-ups)
No language-specific SDK. The evaluate endpoint is plain HTTP. Wrapping it in language-specific clients (Go, Node, Python) is a separate piece of work.
No targeting rules beyond flat rollout percentage. No user-attribute-based targeting (e.g. "50% of users on plan X"), only identifier-based consistent-hash bucketing.
No dedicated flag change history. Flag create/update/delete operations are captured by the platform's existing generic audit log (GET /api/v1/audit-log). There is no flag-specific history view beyond that.