Project invitations
View on GitHubProject 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
- 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.
