Bandera - feature flags with multivariate flags, data-defined targeting, scheduling, and more

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!

5 Likes