Bandera Flag Dashboard

Copy Markdown View Source

Bandera ships an optional LiveView dashboard for browsing and managing flags. It lives in the core library but is inert unless your app depends on phoenix_live_view. It ships no JavaScript and needs no asset-pipeline changes: it inherits your app's layout and runs on your existing LiveView socket.

Install in a Phoenix app

The dashboard is a LiveView that runs inside your app. It needs LiveView wired up (a socket + app.js), Bandera configured with a storage backend, and a route mounted behind your auth. Apps generated with mix phx.new --live already have the LiveView pieces (step 2).

1. Add the dependencies

# mix.exs
def deps do
  [
    {:bandera, "~> 0.4"},
    {:phoenix_live_view, "~> 1.0"}
  ]
end

Run mix deps.get. The dashboard only compiles when phoenix_live_view is present — without it, Bandera still works as a plain flag library.

2. Make sure LiveView is wired up

The dashboard reuses your app's LiveView socket and JavaScript; it ships none of its own. If you generated your app with --live this is already done. Otherwise confirm both:

# lib/my_app_web/endpoint.ex
socket "/live", Phoenix.LiveView.Socket
// assets/js/app.js
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"

const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
const liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}})
liveSocket.connect()

Mount the dashboard under a pipeline whose layout loads that app.js (the default :browser pipeline does). Without the socket the page still renders, but toggles, search, and gate editing won't work.

3. Configure a storage backend

The dashboard reads and writes real flags through Bandera, so configure a persistence backend (defaults to in-memory, which is per-node and resets on restart — fine for trying it out, not for production). For example, Ecto:

# config/runtime.exs
config :bandera,
  store: Bandera.Store.TwoLevel,
  persistence: [adapter: Bandera.Store.Persistent.Ecto, repo: MyApp.Repo]

See the README for all backends (Ecto, Redis, in-memory) and cross-node cache-bust notifications.

4. Mount the route behind your auth

# lib/my_app_web/router.ex
import Bandera.Dashboard.Router

scope "/admin" do
  pipe_through [:browser, :require_admin]   # YOUR authentication/authorization
  bandera_dashboard "/flags"
end

The dashboard can toggle production features, so never mount it on an unauthenticated route. You can also hook authorization into the LiveView's own mount lifecycle with :on_mount:

bandera_dashboard "/flags", on_mount: {MyAppWeb.AdminAuth, :ensure_admin}

Mount it more than once (e.g. per environment) by passing a distinct :live_session_name.

5. Visit it

Start your server and open /admin/flags. Flags are created in code/IEx (e.g. Bandera.enable(:my_flag)); the dashboard manages their gates from there.

The dashboard sets no root layout — it inherits the one from your pipeline. By default its CSS is self-contained and inlined, with bandera--prefixed selectors, so it needs no asset-pipeline changes and won't clash with your styles. See Theming to retheme it or switch to daisyUI.

Grouping

Flags are grouped by a name-prefix convention: the part of the flag's name before the first separator is the group. The separator defaults to "_":

config :bandera, dashboard: [group_separator: "_"]

So :billing_checkout_v2 and :billing_invoices appear under billing. Flags with no separator (e.g. :beta) appear under Ungrouped. Set group_separator: nil to disable grouping.

Theming

The dashboard has two themes, chosen by config:

config :bandera, dashboard: [theme: :standalone]   # default

:standalone (default)

A self-contained, inlined stylesheet with bandera--prefixed selectors. No asset pipeline, no daisyUI, no clashes. Colors, radii, and fonts are read as CSS custom properties with built-in fallbacks, so you can retheme with a single rule — no specificity battles, no !important:

:root {
  --bandera-primary: #0ea5e9;     /* buttons, group headers, focus rings */
  --bandera-radius: 6px;          /* cards and inputs */
  --bandera-surface: #ffffff;     /* card background */
  --bandera-border: #e2e8f0;
  /* also: --bandera-fg, --bandera-muted, --bandera-surface-2,
     --bandera-radius-sm, --bandera-shadow, --bandera-primary-fg,
     --bandera-success, --bandera-off, --bandera-danger,
     --bandera-danger-border, --bandera-danger-bg, --bandera-font */
}

The defaults are a fixed light palette. For dark mode, set the variables (e.g. inside your own @media (prefers-color-scheme: dark)), or use the :daisyui theme below.

:daisyui

config :bandera, dashboard: [theme: :daisyui]

In this mode the dashboard emits daisyUI component classes (btn, input, select, alert, …) and no inlined <style>, so it inherits your app's daisyUI theme (including dark mode and brand colors). This requires your app to build daisyUI itself, and — because Bandera ships compiled templates that Tailwind doesn't scan by default — you must add Bandera to your Tailwind sources so the classes are generated:

/* assets/css/app.css (Tailwind v4 / Phoenix 1.8) */
@source "../../deps/bandera/lib";

(Tailwind v3: add "../deps/bandera/lib/**/*.ex" to content in tailwind.config.js.) Without this, the dashboard renders unstyled.

Live updates across nodes

If you've configured Bandera's PhoenixPubSub cache-bust notifications, open dashboards refresh automatically when another node or operator changes a flag.

What you can manage

Per flag: the boolean on/off toggle, per-actor gates, per-group gates, and a percentage rollout (of actors or of time), plus clearing the whole flag. Search filters the list as you type. (Creating brand-new flags from the UI is not in this version — flags are created in code/IEx via Bandera.enable/2.)