A short, practical tour of Bandera's targeting, rollout, and operations features. Every example assumes Bandera is configured with a store (see the README). enable/2, disable/2, and clear/2 accept an optional by: identity (see Audit log).

Fail-open default

enabled?/2 returns false if the store is unreachable. Pass default: true to fail open for a specific call instead — useful for flags that should stay on if your flag backend has a blip:

# returns true if the store errors, instead of the global false
Bandera.enabled?(:checkout, default: true)

Bandera.enabled?(:checkout, for: current_user, default: true)

The default only applies when the lookup fails; a flag that simply isn't set is still false.

Audit log

Bandera.Audit turns Bandera's write telemetry into structured change events. It's opt-in: attach a handler once at boot and forward events wherever you like.

Bandera.Audit.attach(:my_audit, fn event ->
  MyApp.AuditLog.insert!(event)
end)

Each %Bandera.Audit.Event{} carries :action (:enable | :disable | :clear), :flag_name, :options, :result, :actor, and :at (a DateTime). Record who made a change by passing by: to any write:

Bandera.enable(:promo, by: "admin@example.com")
# => %Bandera.Audit.Event{action: :enable, flag_name: :promo, actor: "admin@example.com", ...}

Detach with Bandera.Audit.detach(:my_audit).

Multivariate flags

Instead of on/off, a flag can return one of N named variants, stable per actor (the same actor always lands in the same variant for a given flag). Weights are relative.

Bandera.put_variants(:checkout_button, %{"blue" => 1, "green" => 1, "red" => 2})

case Bandera.variant(:checkout_button, for: current_user) do
  "blue"  -> ...
  "green" -> ...
  "red"   -> ...
end

# No variant gate / no actor / store error -> the :default (nil if unset)
Bandera.variant(:checkout_button, for: current_user, default: "blue")

A weight of 0 means a variant is never served (handy for ramping a variant down to nothing without removing it).

Targeting rules and context

Pass a context map (%{"attribute" => value}) and gate the flag on it with enable(when:). A rule matches only when all of its constraints hold.

Bandera.enable(:eu_pricing, when: [
  {"country", :in, ["DE", "FR", "ES"]},
  {"plan", :eq, "premium"}
])

Bandera.enabled?(:eu_pricing, context: %{"country" => "FR", "plan" => "premium"})
# => true

Supported operators:

OperatorMeaning
:eq, :neqequal / not equal
:in, :not_inmembership in a list
:containssubstring (strings)
:gt, :gte, :lt, :lteordering (numbers or strings)
:matchesunanchored regex match — "admin" matches "superadmin"; use ^/$ for full-string; an invalid pattern never matches

A missing context attribute never matches. Rule gates compose with the usual actor/group/boolean/percentage gates.

Reusable segments

A segment is a named, reusable set of constraints. Define it once, then point any number of flags at it; editing the segment updates every flag that uses it.

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

Segments are expanded at evaluation time against the current context.

Prerequisites

A flag can require another flag to be in a particular state. Prerequisites only veto — the child still needs its own granting gate.

Bandera.enable(:parent_feature)

Bandera.enable(:child_feature, requires: :parent_feature)
Bandera.enable(:child_feature)            # the child's own grant

Bandera.enabled?(:child_feature)          # true only while :parent_feature is on

Require a parent to be off with requires: {:parent, false}. Cycles and broken chains resolve to false rather than looping.

Scheduling

A schedule gate enables a flag only within an ISO-8601 time window (UTC). Either bound may be nil for an open-ended start or end.

Bandera.enable(:black_friday,
  schedule: {"2026-11-27T00:00:00Z", "2026-11-30T23:59:59Z"})

Bandera.enabled?(:black_friday)   # true only inside the window

Comparisons are always in UTC; a malformed stored window fails closed (the flag is simply not enabled by the schedule).

Finding stale flags

Bandera.Usage records, in ETS, the last time each flag was evaluated. Start it in your supervision tree and attach it once at boot:

children = [
  # ...
  Bandera.Usage
]

# after the tree is up:
Bandera.Usage.attach()

Then list flags that haven't been evaluated recently (or ever):

Bandera.stale_flags(older_than: 30)   # flags untouched for 30+ days

Or from the command line:

mix bandera.flags             # list all flags
mix bandera.flags --stale --older-than 30

Bandera.Usage is entirely opt-in: if it isn't running, stale_flags/1 treats every flag as never-evaluated and the mix task prints a warning.