This is a short, practical guide to gating UI and behavior in a Phoenix LiveView app with Bandera. It assumes Bandera is already installed and configured; see the README for that.
LiveViews are just processes, so calling Bandera from one is nothing special:
Bandera.enabled?/2 works in mount/3, handle_event/3, handle_info/2, and
inside ~H templates. What's worth spelling out is wiring the current user in as
the actor, and testing. Both are covered below.
1. Identify the current user
Most flags are evaluated "for" a user. Implement the Bandera.Actor protocol so
Bandera can derive a stable id for your user struct, and Bandera.Group if you
use group gates (e.g. roles):
defimpl Bandera.Actor, for: MyApp.Accounts.User do
def id(user), do: "user:#{user.id}"
end
defimpl Bandera.Group, for: MyApp.Accounts.User do
def in?(user, group_name), do: group_name in user.roles
endThe simplest way to have current_user available in every LiveView is the
on_mount hook generated by mix phx.gen.auth:
# lib/my_app_web/router.ex
live_session :default, on_mount: [{MyAppWeb.UserAuth, :mount_current_user}] do
live "/dashboard", DashboardLive
endThat puts socket.assigns.current_user in place before your mount/3 runs.
2. Check a flag in a LiveView
Read the flag in mount/3, assign the result, and branch in the template with
:if:
defmodule MyAppWeb.DashboardLive do
use MyAppWeb, :live_view
@impl true
def mount(_params, _session, socket) do
user = socket.assigns.current_user
{:ok, assign(socket, :new_dashboard?, Bandera.enabled?(:new_dashboard, for: user))}
end
@impl true
def render(assigns) do
~H"""
<.new_dashboard :if={@new_dashboard?} />
<.classic_dashboard :if={!@new_dashboard?} />
"""
end
endAssigning once in mount/3 (rather than calling Bandera.enabled?/2 directly in
the template) keeps the check out of the render path and makes the value easy to
override in tests.
A reusable helper keeps controllers and LiveViews consistent:
# lib/my_app_web/feature_flags.ex
defmodule MyAppWeb.FeatureFlags do
def assign_flag(socket_or_conn, key, flag, actor) do
Phoenix.Component.assign(socket_or_conn, key, Bandera.enabled?(flag, for: actor))
end
end3. Gate events and actions
Don't rely on the UI alone. Re-check the flag where the action actually happens, so a hidden control can't be triggered by a crafted event:
@impl true
def handle_event("publish", _params, socket) do
if Bandera.enabled?(:publishing, for: socket.assigns.current_user) do
{:noreply, do_publish(socket)}
else
{:noreply, put_flash(socket, :error, "That feature isn't available yet.")}
end
end4. (Optional) React to flag changes live
mount/3 reads the flag once. If you want a running LiveView to pick up a flag
change without a reload, re-read the flag on some trigger, for example a
periodic tick, or a Phoenix.PubSub broadcast you send when toggling flags:
def mount(_params, _session, socket) do
if connected?(socket), do: Phoenix.PubSub.subscribe(MyApp.PubSub, "feature_flags")
{:ok, assign_new_dashboard(socket)}
end
def handle_info(:flags_changed, socket), do: {:noreply, assign_new_dashboard(socket)}
defp assign_new_dashboard(socket) do
assign(socket, :new_dashboard?, Bandera.enabled?(:new_dashboard, for: socket.assigns.current_user))
endFor most apps the mount-time read is enough; reach for this only when live toggling matters.
5. Outside LiveView
The same calls work everywhere: controllers, function components, and plugs:
# controller
if Bandera.enabled?(:beta_export, for: conn.assigns.current_user) do
# ...
end<%!-- a function component receiving the flag as an assign --%>
<.banner :if={@promo_enabled?} />6. Testing LiveViews
The test layer scopes overrides to the test process and its descendants. A
LiveView started by Phoenix.LiveViewTest is a descendant (Phoenix propagates
$callers), so flags you set in the test are visible inside the LiveView. Tests
stay async: true, never touch the database, and clean up automatically.
One-time setup (see the README for details):
# config/test.exs
config :bandera, store: Bandera.Store.ProcessScoped
# test/test_helper.exs
Bandera.Test.start()Then in a LiveView test, declare flags with the @tag feature_flags: tag or set
them imperatively:
defmodule MyAppWeb.DashboardLiveTest do
use MyAppWeb.ConnCase, async: true
use Bandera.Test
import Phoenix.LiveViewTest
setup :register_and_log_in_user
@tag feature_flags: [new_dashboard: true]
test "renders the new dashboard when the flag is on", %{conn: conn} do
{:ok, _live, html} = live(conn, ~p"/dashboard")
assert html =~ "New dashboard"
end
test "falls back to the classic dashboard when off", %{conn: conn} do
{:ok, _live, html} = live(conn, ~p"/dashboard")
assert html =~ "Classic dashboard"
end
test "enables a flag for one actor only", %{conn: conn, user: user} do
enable_flag(:new_dashboard, user)
{:ok, _live, html} = live(conn, ~p"/dashboard")
assert html =~ "New dashboard"
end
endNotes:
@tag feature_flags: [new_dashboard: true]sets a global boolean override, so it's honored even when the LiveView checksenabled?(:new_dashboard, for: user).- Use
enable_flag(flag, actor)/disable_flag(flag, actor)to override for a specific user, useful for testing per-actor or per-group gates. register_and_log_in_useris themix phx.gen.authhelper; it puts the user in the session socurrent_useris assigned.