You take your first three-person agency customer. They want to invite two coworkers, share access to the same projects, and pay one invoice for all three. The single-user model you've been running on for six months no longer works — and bolting "teams" on top of an existing data model after the fact is one of the most expensive refactors a SaaS team can take on.
This feature installs the team layer before that problem becomes painful. Every user gets a personal team automatically, every request carries a Scope that knows the current user and active team, and a team switcher lets people move between contexts cleanly.
A real tenant boundary, from day one
Multi-tenancy is much easier to install early than to retrofit. The hard part is not the schemas — it is propagating "which team am I acting as right now?" through every controller, LiveView, and context call. This feature ships that propagation through Scope so the discipline is built into the request lifecycle from the start, not added by hand at every call site.
What This Feature Adds
MyApp.Teamscontext with team creation, membership management, invitations, and switching- Schemas:
Team,Membership,Member, andInvitation - A
MyApp.Users.Scopestruct that carriescurrent_userand the activecurrent_teamthrough the request - Extensions to the
Userschema:current_team_id,has_one :personal_team,has_many :memberships,has_many :teams, through: [:memberships, :team] - Automatic personal-team creation when a user registers
- An extension to the authentication pipeline that loads
Scopeand assigns it - LiveView components for the team switcher, member management, and invitations
- Database migrations for the team-related tables
How It Fits Into Your Phoenix Application
Scope is the integration point you use everywhere. Context functions take a Scope argument and use it to filter queries:
user = Users.get_user!(id) |> Users.with_memberships()
Users.switch_team(user, team_id)
Teams.create_team_with_membership(attrs, user)
Teams.create_invitation(team, %{email: "user@example.com", role: "member"})
The team switcher LiveView is wired into the app layout. Personal teams (created on registration) and shared teams are first-class — they live in the same teams table with a flag.
Important. Installing this feature does not automatically scope your existing tenant-owned queries. The schemas, scope propagation, and switcher are in place; auditing existing contexts (Posts.list_posts/1, Imports.list_imports/1, etc.) to filter by scope.current_team_id is work you still own.
Installation Notes
- Hard dependency:
authentication. The feature extends theUserschema and authentication pipeline generated byauthentication. - Run
mix ecto.migrateafter install to create the team tables. - The team switcher is added to the app layout. If you have customised the layout, expect to merge the switcher placement.
- Audit your existing queries for tenant scope after install. The scope is available; using it is per-query.
Build This With Live SaaS Kit
Install saas_kit, then mix saaskit.feature.install authentication followed by mix saaskit.feature.install teams to set up multi-tenancy before the first multi-seat customer asks for it.