Project invitations

View on GitHub

Project invitations

The root README.md already narrates the whole invitation flow end to end (token, 7-day expiry, the logged-in-vs-not accept paths, why the invitation email either goes through the normal notification pipeline or a dedicated one-time ProjectInvitationMail depending on whether the address already has an account). This category doesn't repeat that — it's the "how do I extend this" companion, since the flow itself doesn't have one anywhere yet.

Guides, in the order you'd actually need them

  1. Invite multiple emails at onceProject invitationsInvite multiple emails at onceWorked example: turning the single-email "Invite by email" form into a bulk one — paste several addresses, get one invitation per address, with per-address… — worked example turning the single-email invite form into a bulk one, tracing the change through the hook, the component, the validation, and the Service loop.

Two related, already-documented pieces worth knowing about before extending this flow further: ../permissions/03-grant-a-custom-role-in-bulk.mdRoles & permissions (RBAC)Grant a custom role in bulkWorked example: a new "Grant to all current members" action on a custom role, so a project owner/admin can hand every existing member a role (e.g. a newly… covers the custom-role-preassignment side of an invitation, and ../notifications/04-add-a-dedicated-transactional-email.mdNotifications (in-app & email)Add a dedicated transactional emailGuide 02 covers reusing the generic NotificationMail template (one shared subject/body/action-button layout for every NotificationType). Sometimes that's t… covers ProjectInvitationMail's own pattern for a dedicated, preference-independent email.

The architecture in one paragraph

ProjectInvitationService::invite() is the single entry point: it checks MailConfigurationService::isEnabled() first (no working outgoing mail, no invitations — the entire feature is gated on it), deletes any existing pending invitation for the same email/project pair (so re-inviting someone is just calling invite() again — there is no separate "resend" action or endpoint), creates a new ProjectInvitation row with a random 64-character token and a 7-day expires_at, stores the invitation's tier as a system role in the project_invitation_role pivot (there is no role column on project_invitations) next to any pre-assigned custom Roles (ProjectInvitation::tier() / customRoleIds(); custom roles are narrowed with RoleService::customRoleIds() so a system role can't be smuggled in), and fires ProjectInvited (consumed by SendNotificationListener — see ../notifications/README.mdNotifications (in-app & email)Orbit tells users about activity two ways: an in-app notification (the bell dropdown) and an email, per App\Enums\Notifications\NotificationType — per-user…). Accepting one (acceptByToken()) attaches the user to the project, assigns their system role from $invitation->tier() via RoleService::syncSystemRoleForMember(), attaches any pre-assigned custom roles, and marks the invitation accepted_at — called either automatically right after login/register (if the pending_invitation_token session key from a not-logged-in click on the email link is present) or manually by pasting the token into the Members tab's JoinWithCodePanel.