shipanysaas
Documentation
Back
  • Getting started
    • Install and run
    • Project structure
    • Configuration
    • Commands
  • Coding agents
    • Agent skills
  • Authentication
    • Email sign-in
    • OAuth providers
    • Two-step sign-in and passkeys
  • Database
    • Migrations
    • Row-level security
    • Database tests
    • Reading and writing data
  • Features
    • Teams and invitations
    • Email
    • File uploads
    • Blog, docs and changelog
  • Billing
    • Stripe and Lemon Squeezy
    • Pricing plans
    • Webhooks
  • Live demo
  • Teams and invitations

    Team accounts, roles and permissions, invitations by email, ownership transfer and team deletion.

    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.

    Switches

    VariableDefault in .envEffect
    NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTStrueTeam accounts on or off
    NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATIONtrueUsers can create teams
    NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_ONLYfalseSkip the personal workspace; users land in a team
    NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_DELETIONtrueThe owner can delete a team
    NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLINGtrueTeams have a billing page

    Creating a team

    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.

    Roles and permissions

    The seed creates two roles:

    RoleCan
    ownerManage roles, billing, settings, members and invitations
    memberManage 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.

    Invitations

    1. A member with the invites.manage permission invites one or more people by email, each with a role.
    2. The app sends the invitation email itself (template 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.
    3. The invited person signs in or creates an account with that email and accepts on /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:

    • Invitations expire after 7 days (expires_at default in 07-invitations.sql). From the members page you can renew, change the role of, or delete a pending invitation.
    • The invitation token is readable by members of the team, so the token alone is not enough for one-click sign-in. The accept route also needs the signature, an HMAC made with a key derived from SUPABASE_SECRET_KEY (invitation-signature.ts). Only the link that was emailed can sign the invitee in.
    • On a per-seat plan, the seat quantity is set to the current member count when someone accepts an invitation or a member is removed (account-per-seat-billing.service.ts).
    • You can add rules that block invitations, for example "team must have a subscription", in the policy registry at packages/features/team-accounts/src/server/policies/policies.ts. None are switched on by default.

    Ownership and deletion

    • Transfer ownership. The primary owner can hand the team to another member. It needs a one-time code sent by email.
    • Delete the team. The primary owner can delete the team when NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_DELETION=true. It also needs an emailed code.
    • Leave the team. Any member except the primary owner can leave.

    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.

    Tests

    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.