ESC
Type to search...
S
Soli Docs

Feature Flags

Toggle features on and off at runtime — no redeploy. Ramp a feature out to a percentage of users, allowlist a beta group, or kill it instantly when something breaks.

Flip features live

Flags live in the shared cache (SoliKV), so every worker sees the same value and changes take effect immediately. Feature flags ship as a stdlib module in every new app at stdlib/feature_flags.sl — import it where you need it.

1 Quick start

Import the module, then gate any code path on a flag:

import "../../stdlib/feature_flags.sl"

# In a controller action
if FeatureFlags.enabled?("checkout_v2", user: current_user["id"])
  return render("checkout/v2")
end

return render("checkout/v1")

Manage flags from a console, an admin action, or config/application.sl at boot:

FeatureFlags.enable("checkout_v2")            # on for everyone
FeatureFlags.disable("checkout_v2")           # global kill-switch
FeatureFlags.set_rollout("checkout_v2", 25)   # 25% of users (stable per user)
FeatureFlags.enable_for("checkout_v2", "u_42")     # allowlist a single user
FeatureFlags.enable_group("checkout_v2", "beta")   # allowlist a group

2 How a flag is evaluated

FeatureFlags.enabled?(name, user:, groups:) resolves in this order — the first rule that matches wins:

  1. 1.Environment override. If SOLI_FEATURE_<NAME> is set, it decides the answer outright and the cache is never touched.
  2. 2.Unknown flag. A flag that was never stored is off.
  3. 3.Kill-switch. A flag whose on is false is off, regardless of rollout or allowlist.
  4. 4.Allowlists. If the user is on the user allowlist, or any of groups is on the group allowlist, the flag is on — allowlists always beat the rollout percentage.
  5. 5.Percentage rollout. If a rollout is set below 100%, the user is bucketed deterministically. Anonymous traffic (no user) is never in a partial rollout.
  6. 6.Plain on. Otherwise the flag's on value applies.

3 Percentage rollout

set_rollout(name, percent) turns a flag on for a stable slice of users. Membership is derived from a hash of the flag name and the user id, so a given user always gets the same answer — and as you ramp the percentage up, users who were already in stay in:

FeatureFlags.set_rollout("new_dashboard", 10)   # 10% see it
# …later, once it looks healthy…
FeatureFlags.set_rollout("new_dashboard", 50)   # the original 10% are still in
FeatureFlags.set_rollout("new_dashboard", 100)  # everyone

Because bucketing is keyed on the user id, a logged-out visitor is never counted in a partial rollout — pass a stable id when you want anonymous visitors bucketed too.

4 Environment overrides

SOLI_FEATURE_<NAME> forces a flag without touching the cache — ideal for CI, local development, and emergency kill-switches. The env name is SOLI_FEATURE_ plus the flag name upper-cased:

SOLI_FEATURE_CHECKOUT_V2=1   # force on
SOLI_FEATURE_CHECKOUT_V2=0   # force off

Truthy values are 1, true, on, yes (case-insensitive); anything else reads as off. Keep flag names identifier-like so they map cleanly to environment variable names.

Fail-safe by design

Reads never throw on the request hot path. If the cache is unreachable, enabled? treats the flag as unconfigured and returns off (after honoring any env override) — a cache outage degrades features gracefully instead of 500-ing the request. Flags persist far beyond the cache's default 1-hour TTL: ~10 years by default, override with SOLI_FEATURE_TTL (seconds).

5 API reference

Method Description
enabled?(name, user:, groups:)Is the flag on for this user/groups? Returns a Bool; fails safe to false.
enable(name)Turn the flag on for everyone.
disable(name)Global kill-switch — off for everyone.
set_rollout(name, percent)Turn on for a stable percent (0–100) of users.
enable_for(name, user)Add a user to the allowlist (beats the rollout %).
enable_group(name, group)Add a group to the allowlist.
get(name)Raw config hash, or null if unset.
clear(name)Forget the flag (env overrides still apply).