When I want to ship a risky change to a subset of accounts first, I need a flag I can flip at runtime instead of a deploy I cannot undo. A boolean in the database is not enough either, because I want to target a specific user or a whole team without writing custom queries every time.
In this tutorial I install the fun_with_flags feature with Live SaaS
Kit, enable a flag for one user and then for an entire team, and gate a
LiveView behind it. The starting point is a project with authentication
and teams already installed, because those features provide the actors
the flag library reads.
Step 1 - Install the fun_with_flags feature
First step is to run the installer.
mix saaskit.feature.install fun_with_flags
mix deps.get
Two packages land in mix.exs.
# mix.exs
{:fun_with_flags, "~> 1.13.0"},
{:fun_with_flags_ui, "~> 1.1.0"},
The installer also creates the migration for the flag persistence table.
# priv/repo/migrations/TIMESTAMP_create_feature_flags_table.exs
create table(:fun_with_flags_toggles, primary_key: false) do
add :id, :bigserial, primary_key: true
add :flag_name, :string, null: false
add :gate_type, :string, null: false
add :target, :string, null: false
add :enabled, :boolean, null: false
end
create index(
:fun_with_flags_toggles,
[:flag_name, :gate_type, :target],
unique: true,
name: "fwf_flag_name_gate_target_idx"
)
The installer runs mix ecto.migrate for me, so there is no extra step
here.
Step 2 - Look at the config
The feature injects a persistence block and a cache-bust block into
config/config.exs. The cache-bust part uses the application's Phoenix
PubSub so flag changes propagate across nodes in a cluster.
# config/config.exs
config :fun_with_flags, :persistence,
adapter: FunWithFlags.Store.Persistent.Ecto,
repo: MyApp.Repo,
ecto_primary_key_type: :id
config :fun_with_flags, :cache_bust_notifications,
enabled: true,
adapter: FunWithFlags.Notifications.PhoenixPubSub,
client: MyApp.PubSub
Note that the FunWithFlags supervisor starts itself when the application
loads. I do not need to add a child to application.ex.
Step 3 - Confirm the actors
The feature generates two FunWithFlags.Actor implementations because
authentication and teams are installed.
# lib/my_app/users/user_flag_actor.ex
defimpl FunWithFlags.Actor, for: MyApp.Users.User do
def id(%{id: id}), do: "user:#{id}"
end
# lib/my_app/teams/team_flag_actor.ex
defimpl FunWithFlags.Actor, for: MyApp.Teams.Team do
def id(%{id: id}), do: "team:#{id}"
end
The reason for two implementations is that the same flag can be enabled for one specific user, for one whole team, or for both. So I can roll a change out to my own account first, then to an entire pilot team, before flipping it on globally.
Step 4 - Enable a flag for a user, then for a team
There is a MyApp.Feature wrapper that knows how to read a flag with a
scope-shaped context.
# lib/my_app/feature.ex
def enabled?(flag, %{team: team, user: user})
when not is_nil(team) and not is_nil(user) do
FunWithFlags.enabled?(flag, for: team) || FunWithFlags.enabled?(flag, for: user)
end
To enable a flag I call FunWithFlags.enable/2 directly from iex, with
either a user or a team as the actor.
# iex -S mix
user = MyApp.Users.get_user_by_email("me@example.com")
team = MyApp.Teams.get_team!(user.current_team_id)
FunWithFlags.enable(:new_dashboard, for_actor: user)
FunWithFlags.enable(:new_dashboard, for_actor: team)
The first call targets one specific user. The second targets every member of that team because of the team actor implementation from step 3.
Step 5 - Gate a LiveView behind the flag
With MyApp.Feature.enabled?/2 in place, I gate a LiveView render based
on the current scope.
# lib/my_app_web/live/dashboard_live.ex
def render(%{current_scope: scope} = assigns) do
if MyApp.Feature.enabled?(:new_dashboard, scope) do
~H"""
<.new_dashboard scope={@current_scope} />
"""
else
~H"""
<.classic_dashboard scope={@current_scope} />
"""
end
end
scope already carries the :user and :team keys the wrapper expects,
so the function returns true when either the user or the team has the
flag on, and false otherwise.
Step 6 - Open the flag dashboard
The feature also mounts the FunWithFlags UI through fun_with_flags_ui.
With the admin feature installed, the router exposes it under the
admin pipes.
# lib/my_app_web/router.ex
scope path: "/admin/feature-flags" do
pipe_through [:browser, :require_authenticated_user, :require_admin_user]
forward "/", FunWithFlags.UI.Router, namespace: "admin/feature-flags"
end
So as a superuser, I open /admin/feature-flags and see the
:new_dashboard flag with the actors I enabled in step 4. From that page
I can flip flags on and off without touching iex. However, this
dashboard does not include any test-mode helpers — flags set during a
test run persist between tests unless I clear them explicitly in setup.
Final Result
I can now flip :new_dashboard on for a single user, a whole team, or
globally, and the LiveView reacts on the next render. A natural next step
is to install payments and stripe_subscription, then use a flag like
:billing_v2 to roll the new checkout flow out gradually.