Bandera keeps the same public API and gate model you already use, so migration
is mostly a find-replace plus a config move. The one structural difference is
that all configuration is read at runtime. There is no compile_env, so
config can live in config/runtime.exs and change without a recompile.
This guide walks through the change end to end. Most apps only need steps 1-3.
At a glance
| fun_with_flags | Bandera |
|---|---|
{:fun_with_flags, "~> x.y"} | {:bandera, "~> 0.4.0"} |
FunWithFlags.enabled?/2, enable/2, disable/2, clear/2 | identical on Bandera |
FunWithFlags.Actor / FunWithFlags.Group | Bandera.Actor / Bandera.Group |
config :fun_with_flags, :cache, ... | config :bandera, cache: [...] |
FunWithFlags.Store.Persistent.Ecto | Bandera.Store.Persistent.Ecto |
FunWithFlags.Store.Persistent.Redis | Bandera.Store.Persistent.Redis |
FunWithFlags.Notifications.Redis | Bandera.Notifications.Redis |
FunWithFlags.Notifications.PhoenixPubSub | Bandera.Notifications.PhoenixPubSub |
Step 1: Swap the dependency
In mix.exs, replace the dependency:
# before
{:fun_with_flags, "~> 1.0"},
# after
{:bandera, "~> 0.4.0"},Keep whichever backend deps you already had (ecto_sql, redix,
phoenix_pubsub). They are optional in Bandera too; add only what you use.
If you adopt the test layer (step 7), also add:
{:nimble_ownership, "~> 1.0", only: :test}Then run mix deps.get.
Step 2: Rename the API calls
The function names and signatures are unchanged; only the module changes.
Find-replace FunWithFlags → Bandera across your code:
# before
FunWithFlags.enable(:checkout)
FunWithFlags.enabled?(:beta, for: current_user)
# after
Bandera.enable(:checkout)
Bandera.enabled?(:beta, for: current_user)All of these carry over with identical behavior:
enabled?/2, enable/2, disable/2, clear/2, get_flag/1, all_flags/0,
all_flag_names/0. The gate options (for_actor:, for_group:,
for_percentage_of: {:time | :actors, ratio}) are the same.
Step 3: Move the configuration
Change the config key from :fun_with_flags to :bandera. Bandera reads each
setting as a keyword under the app, and reads it at runtime, so you can keep it
in config/config.exs or move it to config/runtime.exs.
# before (fun_with_flags)
config :fun_with_flags, :cache,
enabled: true,
ttl: 900
config :fun_with_flags, :persistence,
adapter: FunWithFlags.Store.Persistent.Ecto,
repo: MyApp.Repo
config :fun_with_flags, :cache_bust_notifications,
enabled: true,
adapter: FunWithFlags.Notifications.PhoenixPubSub,
client: MyApp.PubSub# after (Bandera)
config :bandera,
cache: [enabled: true, ttl: 900],
persistence: [
adapter: Bandera.Store.Persistent.Ecto,
repo: MyApp.Repo
],
cache_bust_notifications: [
enabled: true,
adapter: Bandera.Notifications.PhoenixPubSub,
client: MyApp.PubSub
]Defaults if you omit a section: in-memory store, cache on (900s TTL),
notifications off. Call Bandera.reload_config/0 to re-read config at runtime.
Note on Redis connection options. fun_with_flags uses one shared
config :fun_with_flags, :redis, [...]. Bandera nests the Redix options under whichever component uses them:config :bandera, persistence: [adapter: Bandera.Store.Persistent.Redis, redis: [host: "localhost", port: 6379]], cache_bust_notifications: [enabled: true, adapter: Bandera.Notifications.Redis, redis: [host: "localhost", port: 6379]]
Step 4: Re-implement the protocols
If you defined FunWithFlags.Actor / FunWithFlags.Group for your structs,
re-implement them under the Bandera namespace. The callbacks are the same:
id/1 for actors, in?/2 for groups:
# before
defimpl FunWithFlags.Actor, for: MyApp.User do
def id(user), do: "user:#{user.id}"
end
defimpl FunWithFlags.Group, for: MyApp.User do
def in?(user, group_name), do: group_name in user.roles
end
# after
defimpl Bandera.Actor, for: MyApp.User do
def id(user), do: "user:#{user.id}"
end
defimpl Bandera.Group, for: MyApp.User do
def in?(user, group_name), do: group_name in user.roles
endBandera ships built-in implementations for binaries, integers, and maps with an
:id / :groups key, just like fun_with_flags.
Step 5: Persistence data
The Ecto table schema is the same shape as fun_with_flags (flag_name,
gate_type, target, enabled), so you have two options:
Option A: keep your existing table. Point Bandera at it by name and skip any data migration:
config :bandera,
persistence: [
adapter: Bandera.Store.Persistent.Ecto,
repo: MyApp.Repo,
ecto_table_name: "fun_with_flags_toggles"
]⚠ Boolean gate compatibility note. FunWithFlags stored boolean gates with a different internal sentinel than Bandera uses. If any flags had their boolean state set while FunWithFlags was active, your table may have duplicate boolean gate rows that cause the toggle to appear stuck. Run the one-time cleanup migration after switching:
mix bandera.gen.fix_fun_with_flags_migration mix ecto.migrateThis generates and runs a migration that normalises any legacy rows. It is safe to run on a table that has already been fully migrated — it finds nothing to change.
Option B: create a fresh Bandera table and copy your rows over. Generate the table from a migration:
defmodule MyApp.Repo.Migrations.CreateBanderaFlags do
use Ecto.Migration
def up, do: Bandera.Ecto.Migrations.up()
def down, do: Bandera.Ecto.Migrations.down()
endThe table name defaults to "bandera_flags" and is read from runtime config.
For Redis persistence, Bandera uses its own key prefix
(bandera:flag:<name> hashes plus a bandera:flag_names set). Either re-create
flags via the API after cutover, or migrate keys to the new prefix.
Step 6: Notifications
Swap the notification adapter module names; the behavior (cross-node cache-busting) is unchanged:
FunWithFlags.Notifications.Redis→Bandera.Notifications.RedisFunWithFlags.Notifications.PhoenixPubSub→Bandera.Notifications.PhoenixPubSub
The Phoenix.PubSub adapter still takes your PubSub server via client:.
Step 7: Tests (the setup is different)
This is the one area where the migration is more than a rename. With
fun_with_flags, flag state is global (shared ETS/DB), so toggling a flag in one
test is visible to every other test. The usual workarounds are running flag
tests with async: false and clearing flags manually (e.g. FunWithFlags.clear/1
in setup/on_exit). Tests that write flags can also deadlock under the Ecto
SQL sandbox.
Bandera replaces that model entirely with an async-safe, process-scoped test
layer. Overrides are scoped to the test process (and its spawned tasks), so
async: true tests don't bleed into each other, toggling a flag never touches
the database, and cleanup is automatic when the test process exits, with no
setup or on_exit plumbing.
Because the model is different, you need to opt into it explicitly (this has no fun_with_flags equivalent):
# config/test.exs: select the process-scoped store
config :bandera, store: Bandera.Store.ProcessScoped
# test/test_helper.exs: start the override server once
Bandera.Test.start()Add the test-layer dependency to mix.exs (see step 1):
{:nimble_ownership, "~> 1.0", only: :test}Then drop the async: false you needed for flag tests and use Bandera.Test:
defmodule MyApp.CheckoutTest do
use ExUnit.Case, async: true
use Bandera.Test
@tag feature_flags: [checkout: true]
test "feature on via tag" do
assert Bandera.enabled?(:checkout)
end
test "toggle in the body" do
enable_flag(:beta)
assert Bandera.enabled?(:beta)
end
endExisting tests that call Bandera.enable/2, Bandera.disable/2, etc. keep
working unchanged: when Bandera.Store.ProcessScoped is configured, those calls
are transparently redirected to the process-scoped overrides instead of the
shared store.
Done
Steps 1-4 cover most apps; add 5-7 as needed. Same behavior as before, with config now resolved at runtime.