Invitations

Add people to your team by inviting them with their email address and a role. The invitee receives a secure link and joins once they accept while logged in as the invited address.

Who can invite

Inviting requires the manage_team capability, so only the owner and admins can send invitations. See Roles & Permissions.

Sending an invitation

Dashboard β†’ Settings β†’ Team (/dashboard/settings?tab=team)

  1. Enter the invitee's email address.
  2. Choose a role: admin, member, or viewer. (owner is never assignable; an invalid value falls back to member.)
  3. Send the invitation.

What happens on the backend:

  • The email is normalized (trimmed, lowercased) and validated.
  • If that person is already a member of the team, the invite is rejected with an error.
  • The seat limit is checked: active members plus pending invitations must be below the owner's plan limit (max_team_members). If the team is full, you are asked to upgrade. See Seats & Billing.
  • A pending invitation is created (or refreshed) for the (team, email) pair, and an email with a unique link is sent.

Invitations are upserted per email: re-inviting the same address refreshes the token, role, and expiry and resets the invite to pending β€” you never accumulate duplicate invites for one person.

Invitation lifecycle

State Meaning
pending Sent, not yet accepted or declined, and not expired
accepted The invitee joined the team
declined The invitee declined the link
expired The link's expiry (default 7 days / 168 hours) has passed

Only invitations that are pending and unexpired can be accepted. Expired or already-actioned links show "This invitation is invalid or has expired."

Token security

  • The invitation link carries a secret token; the database stores only its SHA-256 hash, so a leaked database row cannot be turned back into a working link.
  • Accepting requires being logged in as the invited email. A link forwarded to or stolen by someone else cannot be redeemed β€” acceptance checks that your session email matches the invited address.
  • Tokens expire after 7 days by default.

The accept flow

  1. The invitee clicks the link: GET /team/invite/{token}.
  2. The invitation page shows the team name and the invite details.
    • If the invitee is not logged in, they are prompted to log in (or register) with the invited email first.
    • If they are logged in but with a different email, acceptance is refused with a message naming the correct address.
  3. The invitee accepts: POST /team/invite/{token}/accept.
  4. Inside a single transaction:
    • The team is confirmed to still exist.
    • The seat limit is re-checked (guards against the team filling up between send and accept).
    • The membership row is created with the invited role (clamped to admin/member/viewer).
    • The user's active team switches to this team.
    • The invitation is marked accepted.
  5. The invitee lands on the dashboard, now operating inside the new team.

If the team is full at accept time, the invitee sees: "This team has reached its member limit. Ask the owner to upgrade the plan."

Declining

The invitee can decline from the invitation page: POST /team/invite/{token}/decline. This marks the invitation declined and does not require login.

Managing members after they join

Owners and admins can manage members from Dashboard β†’ Settings β†’ Team:

  • Change role β€” Set a member to admin, member, or viewer. You cannot change your own role, and the owner's role cannot be changed here (use ownership transfer).
  • Remove member β€” Remove a non-owner member. They are automatically re-pointed to their own personal team so they are never locked out. You cannot remove yourself this way (use "Leave team"); the owner cannot be removed.

Leaving and transferring

  • Leave team β€” Any non-owner member can leave (POST /dashboard/settings/team/leave); their active team reverts to their personal team. The owner cannot leave β€” they must transfer ownership first.
  • Transfer ownership β€” The owner hands ownership to an existing member (POST /dashboard/settings/team/transfer, requires transfer_ownership). In one transaction, the new owner becomes owner and the previous owner becomes admin, preserving the exactly-one-owner invariant. The target must already be a member of the team.

Next Steps