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— joinsusersandteamswith arole.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.