FullstackPhoenix

Teams and Multi-Tenancy for Phoenix

phoenixteamsmulti-tenancysaas

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.Teams context with team creation, membership management, invitations, and switching
  • Schemas: Team, Membership, Member, and Invitation
  • A MyApp.Users.Scope struct that carries current_user and the active current_team through the request
  • Extensions to the User schema: 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 Scope and 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 the User schema and authentication pipeline generated by authentication.
  • Run mix ecto.migrate after 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.