Invitation Lifecycle
Status Flow
pending � accepted (successful acceptance)
pending � rejected (user declined)
pending � cancelled (admin cancelled)
pending � expired (expiration date passed)
Status Descriptions
pending:
- Initial state when invitation is created
- User can accept or decline
- Admin can cancel
- Automatically expires after 7 days
accepted:
- User has accepted the invitation
- Membership has been created
- Cannot be undone (must remove member instead)
rejected:
- User has declined the invitation
- No membership created
- Cannot be undone (must create new invitation)
cancelled:
- Admin has cancelled the invitation
- User can no longer accept
- Cannot be undone (must create new invitation)
expired:
- Invitation expiration date has passed
- User can no longer accept (but can decline)
- Must create new invitation to re-invite
Security
Token Hashing
Invitation tokens are hashed using SHA256 before storage for security:
Plaintext Token (stored in email): 550e8400-e29b-41d4-a716-446655440000
Hashed Token (stored in DB): SHA256(plaintext_token)
Security Benefits:
- Database compromise doesn’t expose usable tokens
- Tokens cannot be used if database is leaked
- One-way hashing prevents token recovery
Token Generation:
// Plaintext token (shown to user once)
const token = crypto.randomUUID();
// Hash for storage
const tokenHash = crypto
.createHash('sha256')
.update(token)
.digest('hex');
Token Verification:
// Hash provided token
const providedHash = crypto
.createHash('sha256')
.update(providedToken)
.digest('hex');
// Compare with stored hash
if (providedHash === storedHash) {
// Valid token
}
Expiration
Default Expiration: 7 days from creation
Expiration Constant:
const INVITATION_EXPIRY_DAYS: i64 = 7;
Expiration Calculation:
let expires_at = Utc::now() + ChronoDuration::days(INVITATION_EXPIRY_DAYS);
Expiration Check:
let expires_at = chrono::DateTime::parse_from_rfc3339(&invitation.expires_at)
.ok()
.map(|dt| dt.with_timezone(&Utc))
.unwrap_or_else(Utc::now);
if expires_at < Utc::now() {
return Err(AppError::BadRequest("Invitation has expired".to_string()));
}
Best Practices
For Organization Admins:
- Only invite users with legitimate need for access
- Assign the minimum required role (principle of least privilege)
- Review and cancel unused pending invitations regularly
- Monitor invitation acceptance rates
- Use descriptive organization names for clarity in emails
For Security:
- Never share invitation tokens in public channels
- Tokens should only be sent via secure email
- Implement rate limiting on invitation endpoints
- Monitor for suspicious invitation patterns
- Set up alerts for failed acceptance attempts
For Integration:
- Always verify invitation tokens before acceptance
- Handle expired invitations gracefully in UI
- Provide clear error messages for invalid tokens
- Log invitation events for audit trails
- Test email delivery before production use