Every user has a personal account. Team accounts are shared workspaces that several users belong to, each with a role. The code is in packages/features/team-accounts, the pages in apps/web/app/[locale]/home/[account]/, and the tables and rules in apps/web/supabase/schemas/03-accounts.sql to 07-invitations.sql.
| Variable | Default in .env | Effect |
|---|---|---|
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS | true | Team accounts on or off |
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION | true | Users can create teams |
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_ONLY | false | Skip the personal workspace; users land in a team |
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_DELETION | true | The owner can delete a team |
NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING | true | Teams have a billing page |
Users create a team at /home/create-team. The team gets a slug from its name, and its pages live at /home/<slug>: home, members, settings and billing. The creator becomes the primary owner.
The seed creates two roles:
| Role | Can |
|---|---|
owner | Manage roles, billing, settings, members and invitations |
member | Manage settings and invitations |
Permissions are checked by the database, through the has_permission function in the row-level security policies, so they also hold for API calls. Members act only on people ranked below them, and nobody can remove or demote the primary owner. To add a role or change what a role can do, write a migration that inserts into public.roles and public.role_permissions.
invites.manage permission invites one or more people by email, each with a role.packages/email-templates/src/emails/invite.email.tsx, sent through the configured mailer). The link goes to /join/accept and carries the invitation token plus a signature made on the server./join. If the app has no email-only sign-in method (magic link or code), a new user is then asked to set up a password or another method (/identities).Details worth knowing:
expires_at default in 07-invitations.sql). From the members page you can renew, change the role of, or delete a pending invitation.SUPABASE_SECRET_KEY (invitation-signature.ts). Only the link that was emailed can sign the invitee in.account-per-seat-billing.service.ts).packages/features/team-accounts/src/server/policies/policies.ts. None are switched on by default.NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_DELETION=true. It also needs an emailed code.The codes are stored hashed in public.nonces, work once, and are revoked after 5 wrong attempts. Deleting a personal account uses the same kind of code.
team-accounts.test.sql, memberships.test.sql, invitations.test.sql, update-membership.test.sql, delete-membership.test.sql and transfer-ownership.test.sql cover these rules in the database. The Playwright specs in apps/e2e/tests/team-accounts and apps/e2e/tests/invitations cover the pages.