This tutorial walks through a multi-tenant product where a team is the customer boundary (workspace, organization, account). You get membership-scoped team access, permission checks, team selection UX, and invitations. If you have not installed Stack yet (handler routes,Documentation Index
Fetch the complete documentation index at: https://stackauth-e0affa27-chore-move-mcp-to-a-sep-app.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
StackProvider, environment variables), start with Build a SaaS with Stack Auth, then continue here.
What you will have at the end
- Teams listed and resolved for the signed-in user (safe deep links like
/team/[teamId]). - Team creation from the client (with dashboard settings) and an optional server provisioning pattern.
- A team switcher pattern using
SelectedTeamSwitcherwith deep links and optionalselectedTeamupdates. - RBAC checks aligned with dashboard-defined team permissions, with server-side enforcement before mutations.
- A path to invite collaborators and accept invitations.
Prerequisites
- Stack Auth installed as in Build a SaaS with Stack Auth (Next.js App Router examples below assume
stackServerAppinstack/server.tsand@stackframe/stackin the app). - A project in the dashboard where you can edit Teams and Team permissions.
On the server, prefer
user.getTeam(id) and user.listTeams() from stackServerApp.getUser() so you only ever load teams the current user belongs to. stackServerApp.getTeam / stackServerApp.listTeams operate at project scope (useful for admin or provisioning, not for normal tenant pages).1. Turn on teams in the dashboard
In your Stack project:- Teams - Enable team features and review defaults (for example whether a personal team is created on sign-up), per Teams.
- Client-side team creation - If the browser will call
user.createTeam, enable client team creation in team settings (same guide). - Team permissions - In Team permissions, define the actions your product needs (for example
export_reports, nested under a role likeadmin). Add Stack system permissions where needed; their IDs start with$(for example$invite_members,$read_members,$update_team). See RBAC.
2. List teams and resolve a team from a URL
Use the current user as the scope:listTeams / useTeams, and getTeam / useTeam for one id.
- Server Component
- Client Component
app/team/[teamId]/page.tsx
params as a Promise (newer Next.js), await the params object before reading teamId.
Index page: all workspaces
- Server
- Client
app/team/page.tsx
3. Create teams
Signed-in user creates a team (client)
After enabling client-side team creation, the creator is added as a member with your project’s default creator permissions:components/create-workspace-button.tsx
Provision a team without a browser session (server)
For imports, support tools, or other server jobs,stackServerApp.createTeam creates a team at project scope (see Teams); wire membership separately if your flow requires it.
scripts/provision-team.example.ts
4. Team selection and deep links
Stack tracks aselectedTeam on the user for “current workspace” UX. For B2B apps, deep links (/team/[teamId]) are usually recommended so shared URLs always refer to the same tenant; see Team selection.
SelectedTeamSwitcher updates selectedTeam when selectedTeam is passed, optionally navigates via urlMap, and can skip updating stored selection with noUpdateSelectedTeam:
components/team-switcher.tsx
noUpdateSelectedTeam when you only want navigation (for example “open workspace” without changing the persisted default). Details and examples are in Team selection.
5. Enforce RBAC (server + client)
Define permissions in the dashboard, then branch ongetPermission / usePermission on the user, scoped to a team.
Replace export_reports with a permission you created.
- Server Component
- Client Component (UX only)
app/team/[teamId]/reports/page.tsx
listPermissions / usePermissions (RBAC).
Server action before a mutation
app/actions/reports.ts
6. Invitations and membership
Users with$invite_members can invite by email from the Team object (options object in the SDK):
components/pending-invites.tsx
team.listInvitations / team.useInvitations (Teams).
7. Metadata, profiles, and members
Teams supportclientMetadata, serverMetadata, and clientReadOnlyMetadata on team.update (Teams). Per-team member display uses getTeamProfile / useTeamProfile on the user; listing members uses team.listUsers / team.useUsers and requires $read_members on the client.
Related guides
| Topic | Guide |
|---|---|
| Auth bootstrap and route protection | Build a SaaS with Stack Auth |
| Teams API reference | Teams |
SelectedTeamSwitcher and URL strategies | Team selection |
| Permission modeling and nesting | RBAC |
| REST without the SDK | API overview |
| Pre-launch | Launch checklist |
FAQ
When should I use user.getTeam vs stackServerApp.getTeam?
When should I use user.getTeam vs stackServerApp.getTeam?
For product pages and APIs acting as the signed-in user, use
(await stackServerApp.getUser()).getTeam(id) so membership is enforced by Stack. Use stackServerApp.getTeam only when you intentionally need project-wide team lookup (for example internal admin tools), then apply your own authorization.Why split the client export button into two components?
Why split the client export button into two components?
Do permissions replace checks in my database?
Do permissions replace checks in my database?
Stack permissions answer whether this Stack user may perform an action in this Stack team. You should still scope your rows by
team.id (or equivalent) and validate inputs-Stack does not replace your data model.