FullstackPhoenix

Add an Admin Dashboard to a Phoenix SaaS App

phoenixadminliveviewteamsauthentication

Once a Phoenix SaaS application has real users and teams, I need a place where an operator can look those records up, change roles, and impersonate an account to debug a complaint. In this tutorial I install the admin feature on top of the foundation, sign in as a superuser, and visit the protected /admin dashboard.

The starting point is a project with the authentication, layouts, and teams features already installed and a registered user. The admin feature declares all three as dependencies, so the installer will refuse to run if any of them is missing.

Step 1 - Install the admin feature

First step is to run the installer.

mix saaskit.feature.install admin

The feature adds flop and flop_phoenix to mix.exs for paginated lists, then generates a set of admin LiveViews under lib/my_app_web/live/admin/. The most important ones are:

  • MyAppWeb.Admin.DashboardLive.Index at /admin
  • MyAppWeb.Admin.UserLive.Index at /admin/users
  • MyAppWeb.Admin.TeamLive.Index at /admin/teams
  • MyAppWeb.Admin.AdminLive.Index at /admin/admins
  • MyAppWeb.Admin.DeveloperLive.Index at /admin/developers
  • MyAppWeb.Admin.SettingLive.Edit at /admin/settings

Note that this feature does not add a migration. It relies on the existing users and teams tables and the role enum that authentication already installed.

So after the install completes, I fetch the new deps.

mix deps.get

Step 2 - Look at the router changes

The installer injects a new admin scope into router.ex. The block is worth reading because it tells me exactly which pipes and on_mount hooks guard the dashboard.

# lib/my_app_web/router.ex
scope "/admin", MyAppWeb.Admin do
pipe_through [:browser, :require_authenticated_user, :require_admin_user]
live_session :admin,
on_mount: [
{MyAppWeb.UserAuth, :mount_current_scope},
{MyAppWeb.AdminAuth, :require_admin_user}
] do
live "/", DashboardLive.Index, :index
live "/users", UserLive.Index, :index
live "/teams", TeamLive.Index, :index
live "/admins", AdminLive.Index, :index
# other admin routes
post "/impersonate/:id", UserImpersonationController, :create
end
live_dashboard "/live_dashboard", metrics: MyAppWeb.Telemetry
end

The :require_admin_user plug checks current_scope.user.role. It allows in :admin and :superuser and rejects everyone else. So a regular :user account hitting /admin is redirected away.

Step 3 - Make sure I have an admin account

The admin feature does not promote anyone for me. It expects that the authentication feature already handled the first account through first_user_superuser: true, or that I promote an existing user explicitly.

If I am working in a fresh project, I just register the first user and the authentication feature gives them :superuser automatically. If I am adding admin to an established project, I promote an existing user from iex.

# iex -S mix
user = MyApp.Users.get_user_by_email("me@example.com")
MyApp.Admins.create_admin(%{email: user.email})

MyApp.Admins.create_admin/1 is generated by the feature. It sets the user's role to :admin, which is enough to pass the :require_admin_user plug.

Step 4 - Open the dashboard

With these changes in place, I start the server and visit /admin.

mix phx.server

The dashboard at /admin is the home page. From there I can move to /admin/users for a paginated user list, /admin/teams for teams, and /admin/admins to promote or demote others. Each list uses Flop for filtering and pagination, with a search_phrase filter wired into the user table by default.

The Phoenix LiveDashboard lives at /admin/live_dashboard for runtime telemetry, behind the same admin pipes.

Step 5 - Impersonate a user

The user list has an impersonate action that POSTs to /admin/impersonate/:id. The controller generates a session token for the target user and flips a flag in the session.

# lib/my_app_web/controllers/admin/user_impersonation_controller.ex
conn
|> put_session(:impersonating, true)
|> put_session(:user_token, token)
|> put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(token)}")
|> put_flash(:info, "Impersonating user")
|> redirect(to: ~p"/")

So from the admin side I click "Impersonate" on a user row, land on the public home page as that user, and can reproduce whatever they were seeing. The :impersonating session key is what the UI uses to surface an "exit impersonation" banner.

Final Result

I can now sign in as a superuser, open /admin, and manage users, teams, and admins from inside the app. The next feature that hooks into this surface naturally is oban for background jobs, which gives the developer page something real to link to.