Nuda Kit includes a complete invitation system for adding new members to teams. Invitations are sent via email and can be accepted or rejected by recipients.
The invitation system provides:
Email Invitations
Role Assignment
Expiration
Resend & Cancel
| Column | Type | Description |
|---|---|---|
id | Integer | Primary key |
email | String | Invitee's email address |
token | String | Unique invitation token |
team_id | Integer | Foreign key to teams |
role | Enum | Role to assign on acceptance |
expires_at | Timestamp | When invitation expires |
created_at | Timestamp | When invitation was sent |
updated_at | Timestamp | Last update |
| Role | Description |
|---|---|
super-admin | Can manage all members except owner |
admin | Can manage lower-level members |
editor | Can edit team content |
viewer | Read-only access |
owner role cannot be assigned via invitation. Teams can only have one owner.┌─────────────────────────────────────────────────────────────────┐
│ INVITATION FLOW │
└─────────────────────────────────────────────────────────────────┘
1. Admin sends invitation
│
▼
┌─────────────────────┐
│ Validate Request │ ─── Check: Not already invited
│ │ ─── Check: Not already a member
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Create Invitation │ ─── Generate unique token
│ │ ─── Set expiration (24h)
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Queue Email Job │ ─── SendInvitationEmailJob
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Send Email │ ─── Contains invitation link
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Recipient Clicks │ ─── /invitations/{token}
│ Link │
└──────────┬──────────┘
│
┌─────┴─────┐
▼ ▼
┌─────────┐ ┌─────────┐
│ Accept │ │ Reject │
└────┬────┘ └────┬────┘
│ │
▼ ▼
┌─────────┐ ┌─────────┐
│ Add to │ │ Delete │
│ Team │ │ Invite │
└─────────┘ └─────────┘
| Method | Endpoint | Description |
|---|---|---|
POST | /teams/:id/invitations | Send invitation |
GET | /teams/:id/invitations | List pending invitations |
POST | /teams/:id/invitations/:invitationId/resend | Resend invitation |
DELETE | /teams/:id/invitations/:invitationId | Cancel invitation |
| Method | Endpoint | Description |
|---|---|---|
GET | /invitations/:token | Get invitation details |
POST | /teams/:id/invitations/:token/accept | Accept invitation |
POST | /teams/:id/invitations/:token/reject | Reject invitation |
POST /teams/:id/invitations
{
"email": "newmember@example.com",
"role": "editor"
}
Before creating an invitation, the system checks:
{
"message": "team invite sent"
}
The recipient receives an email with:
{FRONTEND_URL}/invitations/{token}GET /invitations/:tokenPOST /teams/:id/invitations/:token/acceptPOST /teams/:id/invitations/:token/accept
{
"message": "team invitation accepted",
"data": {
"teamId": 1
}
}
| Status | Message | Cause |
|---|---|---|
| 422 | "invitation expired" | Invitation is past expiration |
| 409 | "already a member of this team" | User joined via another invite |
| 404 | Not found | Invalid token or wrong email |
POST /teams/:id/invitations/:token/reject
{
"message": "team invitation rejected",
"data": {
"teamId": 1
}
}
The invitation is deleted after rejection.
GET /teams/:id/invitations
Returns all pending invitations for a team.
POST /teams/:id/invitations/:invitationId/resend
Sends a new email with the same token. Useful if the original email was lost or expired.
DELETE /teams/:id/invitations/:invitationId
Deletes the invitation. The link will no longer work.
Tokens are generated using cryptographically secure random bytes:
static generateToken() {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
let result = ''
for (let i = 0; i < 10; i++) {
result += chars.charAt(Math.floor(crypto.randomBytes(1)[0] % chars.length))
}
return result
}
| Action | Required Role |
|---|---|
| Send invitation | Owner, Super Admin, Admin |
| View invitations | Owner, Super Admin, Admin |
| Resend invitation | Owner, Super Admin, Admin |
| Cancel invitation | Owner, Super Admin, Admin |
| Accept invitation | Invited user only |
| Reject invitation | Invited user only |
// When user lands on /invitations/:token
const { data: invitation } = await useInvitationQuery().getInvitation(token)
const { mutate: acceptInvitation } = useTeamMutation().acceptInvitation
acceptInvitation({
teamId: invitation.teamId,
token: invitation.token
})
const { data: invitations } = useTeamQuery().getTeamInvitations(teamId)
Update the expiration duration in SendTeamInvitesController:
const invitation = await team.related('invitations').create({
email: payload.email,
role: payload.role,
token: Invitation.generateToken(),
expiresAt: DateTime.now().plus({ hours: 48 }), // Changed from 24 to 48
})
Edit the template at resources/views/emails/invitation_email_html.edge:
@mjml()
<mjml>
<mj-body>
<!-- Customize invitation email here -->
<mj-section>
<mj-column>
<mj-text>
You've been invited to join our team!
</mj-text>
<mj-button href="{{ invitationUrl }}">
Accept Invitation
</mj-button>
</mj-column>
</mj-section>
</mj-body>
</mjml>
@end