Hi all! I’m excited to share Bandera, a feature flag library I’ve been building.
It came out of four things I kept wanting and not quite having. I wanted flags that turn themselves on and off on a schedule, instead of someone watching the clock for a launch. I wanted variants so I could show different things to different users and keep an experiment stable per person. I kept getting bitten by compile-time config — a flag setting baked into a release artifact means you recompile to change it, and mix release’s :validate_compile_env check will refuse to boot when runtime.exs overrides a compile-time key. I also wanted flag tests that run async: true without leaking state into each other. Bandera grew out of solving those, on top of the usual boolean/actor/group/percentage gates.
The big features:
Scheduling. Hand a flag an ISO-8601 window (UTC) and it activates and sunsets on its own — no one toggling a switch at the start of a sale.
Bandera.enable(:black_friday, schedule: {"2026-11-27T00:00:00Z", "2026-11-30T23:59:59Z"})
Multivariate flags. Serve one of N named variants, weighted and sticky per actor — the same user always sees the same variant, across nodes and restarts (stable SHA-256 bucketing). It’s built for experiments and gradual variant ramps, not just on/off.
Bandera.put_variants(:hero_cta, %{"control" => 9, "treatment" => 1})Bandera.variant(:hero_cta, for: current_user) # => "control" or "treatment", same every time
Everything is runtime config. Cache, TTL, persistence adapter, table name, notifications — all read at runtime from config/runtime.exs, no compile_env anywhere. Change a value without a recompile, and reload_config/0 applies it live. No fighting :validate_compile_env at boot.
Tests run async. Flag state is usually global, so a toggle in one test leaks into the next and you’re stuck with async: false. Bandera ships a process-scoped test layer that scopes overrides to the test process (and any tasks it spawns) — so your flag tests run async: true, never touch the database, and clean themselves up.
use Bandera.Test
@tag feature_flags: [checkout: true]test "checkout is on" do assert Bandera.enabled?(:checkout)end
There’s also data-defined targeting that I lean on a lot: match on arbitrary attributes through an evaluation context (:eq, :in, :gt, :matches, and friends), bundle the rules into reusable named segments, and gate flags on other flags with prerequisites — all stored as data, so you change who sees a feature by writing a row, not shipping code.
Bandera.put_segment(:premium_us, [{"plan", :eq, "premium"}, {"country", :eq, "US"}])Bandera.enable(:new_billing, for_segment: :premium_us)
Bandera.enabled?(:new_billing, context: %{"plan" => "premium", "country" => "US"}) # => true
A few more things in the box:
- Storage: in-memory by default (zero setup), or Ecto (Postgres/SQLite) or Redis — with an ETS cache and cross-node cache-busting in front of any of them.
- A LiveView dashboard to browse and manage flags, with editors for every gate type. No JS, no asset pipeline changes; it mounts behind your own admin auth and lights up automatically if your app uses
phoenix_live_view. - An opt-in audit log, stale-flag detection (
mix bandera.flags --stale), and telemetry on every read and write.
def deps do
[{:bandera, "~> 0.4.0"}]
end
If you’re coming from fun_with_flags, there’s a migration guide that walks through the differences. The api is pretty similar, so in many cases you’ll be done in 10 minutes.
I’d genuinely love feedback — on the API, on what’s missing, on anything that feels off. Thanks for taking a look!






















