FullstackPhoenix

Build a SaaS Foundation With Authentication, Layouts, and Teams

phoenixauthenticationlayoutsteamssaas

If I am starting a new Phoenix SaaS application, I need three things before I can build anything interesting: accounts that sign in, page surfaces that look like an application, and a team scope that records can belong to. In this tutorial I install those three pieces with Live SaaS Kit and end up with a multi-tenant shell that I can hang real product features on.

The starting point is a Phoenix project that already has the saas_kit package installed and the boilerplate token configured. If that is not done, the previous tutorial covers it. The goal of this tutorial is to install authentication, layouts, and teams in the order they depend on each other, and then sign up a first user that gets a personal team for free.

Step 1 - Install authentication

First step is to install the authentication feature. It has no dependencies, so it can go first.

mix saaskit.feature.install authentication

The installer runs mix phx.gen.auth Users User users --live under the hood, then layers extra fields on top: a role enum (:user, :admin, :superuser), profile fields like name and timezone, an embedded data map, and a soft-delete deleted_at. A migration is added under priv/repo/migrations to apply the extra columns.

So I run the migration.

mix ecto.migrate

The feature also writes an admin config block.

# config/config.exs
config :my_app, :admin,
first_user_superuser: true,
admin_emails: []

Note that first_user_superuser: true automatically promotes the first registered user to :superuser. The admin_emails list pins specific addresses to admin roles. I set those before registering my first user.

Step 2 - Install layouts

Next step is to install the layouts feature. It replaces the default single layout with three purpose-built ones.

mix saaskit.feature.install layouts

The installer adds three layout modules.

lib/my_app_web/components/layouts/app.ex
lib/my_app_web/components/layouts/public.ex
lib/my_app_web/components/layouts/session.ex

Layouts.App is the authenticated shell with the logo, theme toggle, and user avatar in the header. Layouts.Session is the minimal layout for the login, registration, and confirmation LiveViews. Layouts.Public is for landing and marketing pages.

No migration is added by this feature. It is pure UI reorganisation. The auth LiveViews from step 1 are rewritten to render inside <Layouts.Session.page>.

Step 3 - Install teams

Now that authentication and layouts are in place, I can install teams. The feature declares dependencies = ["authentication"], so the installer will refuse to run if step 1 was skipped.

mix saaskit.feature.install teams
mix ecto.migrate

The migration adds three new tables and one column.

  • teams — name, personal:boolean, created_by_user_id, with a unique
  • index that allows one personal team per user.
  • team_memberships — joins users and teams with a role.
  • invitations — invitation token, accepted/declined timestamps,
  • team_id, invited_by_user_id.
  • users.current_team_id — the team the user is currently looking at.

The feature also extends the Scope struct so current_team is available inside LiveViews and controllers, and it adds two routes under the authenticated scope.

# lib/my_app_web/router.ex
live "/teams", TeamLive.Index, :index
live "/teams/:team_id/members", MemberLive.Index, :index

Step 4 - Confirm the personal-team-on-signup wiring

The most useful part of teams is the change to register_user/1. The installer pipes the result through a private helper that creates a personal team and an owner membership.

# lib/my_app/users.ex
defp create_personal_team({:ok, user} = result) do
{:ok, team} =
MyApp.Teams.create_team(
user,
%{personal: true, name: "Personal Team"}
)
MyApp.Teams.create_membership(team, user, %{role: :owner})
result
end
defp create_personal_team(result), do: result

The reason is that every signed-in user should immediately have at least one team scope, even when they have not invited anyone. So a fresh account never hits a "you have no team" empty state.

Step 5 - Sign up and verify

With these changes in place, I can start the server and walk through it.

mix phx.server

If I open http://localhost:4000/users/register, I see the Session layout from step 2. I register a first user with my admin email, and that account gets promoted to :superuser by the first_user_superuser rule. After the redirect I land on the App layout from step 2, and the team switcher in the header already shows "Personal Team".

If I open /teams I see one row, with me as owner. If I open /teams/:team_id/members I see myself listed.

Final Result

I now have authentication, a real application shell, and a multi-tenant scope tied to every account. The next sensible step is to add an admin dashboard so I can manage these users and teams from inside the app rather than from iex.