Invitation
Overview
The Invitation domain in comby handles the process of inviting users to join an organization or Tenant. It supports the lifecycle of an invitation, from creation and sending to acceptance or declination. The Invitation aggregate is at the core of this domain, providing the structure for managing invitations, tracking their state, and handling related domain events.
Concept
What is an Invitation?
An Invitation is a secure, token-based mechanism to invite users to join a tenant or workspace. When accepted, the invitation automatically creates an identity for the user with pre-configured group memberships.
Key Characteristics:
- Token-Based Security: Each invitation has a unique, random token
- State-Driven Lifecycle: Tracks invitation states (created, sent, accepted, declined)
- Dual-Level Support: Tenant-level and workspace-level invitations
- Pre-Configured Access: Specify groups during invitation creation
- Email-Linked: Invitations are tied to specific email addresses
Invitation Lifecycle:
Created → Sent → Accepted/Declined
↓ ↓ ↓
Token Email Identity CreatedArchitecture
Hierarchy
System
└── Tenant
├── Invitations (Tenant-Level)
│ ├── Token
│ ├── Email
│ ├── GroupUuids (Tenant groups)
│ └── State
└── Workspaces
└── Invitations (Workspace-Level)
├── Token
├── Email
├── GroupUuids (Tenant groups)
├── WorkspaceGroupUuids (Workspace groups)
└── StateRelated Entities
Invitation is connected to:
- Tenant: Each invitation belongs to a tenant
- Workspace: Invitations can be workspace-specific
- Identity: Creator and target identities
- Account: Accepting user's account
- Group: Tenant-level and workspace-level groups
Domain Model
An Invitation can belong to only one Tenant. However, a user can be invited to multiple Tenants. This design enables a user to be associated with multiple Tenants through separate invitations.
Invitation States
| State | Description |
|---|---|
created | Invitation created but not yet sent |
sent | Invitation sent to recipient (email) |
accepted | User accepted the invitation |
declined | User declined the invitation |
Invitation Types
1. Tenant-Level Invitation
Invites a user to join a tenant with specific tenant-level groups.
Flow:
1. Create invitation with tenant groups
2. Send invitation (email sent)
3. User accepts → Identity created with tenant groups2. Workspace-Level Invitation
Invites a user to join both a tenant AND a specific workspace.
Flow:
1. Create invitation with workspace context
2. Specify both tenant groups AND workspace groups
3. Send invitation
4. User accepts → Identity created + added to workspaceStructure
The Invitation aggregate consists of the following elements:
References:
- GroupUuids: A list of group UUIDs to which the invited account will be added upon accepting the invitation.
- IdentityUuid: The UUID of the identity that created the invitation.
- AccountUuid: The UUID of the account that accepts the invitation.
Value Objects:
- Token: A unique token associated with the invitation, typically used for verification or authentication.
- Email: The email address of the invitee.
- State: Tracks the current state of the invitation, such as created, sent, accepted, or declined.
The domain defines several states to represent the lifecycle of an invitation:
- created: The invitation has been created but not yet sent.
- sent: The invitation has been sent to the invitee.
- accepted: The invitee has accepted the invitation, and the associated account has been added to the relevant groups.
- declined: The invitee has declined the invitation.
These states enable precise tracking of invitation progress and ensure consistent processing across the system.
Use Cases
1. Inviting New Team Member
Scenario: Manager invites new developer to join team
1. Manager creates invitation with "Developers" group
2. System generates unique token and sends email
3. New user receives email with invitation link
4. User clicks link, creates account (if needed)
5. User accepts invitation → Identity created with Developers group2. Workspace Collaboration
Scenario: Project lead invites consultant to specific workspace
1. Lead creates workspace-level invitation
- Tenant groups: ["Members"]
- Workspace groups: ["Project Consultants"]
2. Consultant accepts invitation
3. Consultant gets:
- Identity in tenant with Members permissions
- Workspace membership with Consultant permissions3. Bulk Invitations
Scenario: Onboarding multiple users at once
For each user email:
1. Create invitation with common groups
2. Send invitations (batch email)
3. Track acceptance status
4. Remind users who haven't accepted4. Invitation Expiry & Resending
Scenario: User didn't receive or lost invitation
1. Admin finds old invitation
2. Admin resends invitation (same token)
3. User receives new email with same link
4. User accepts invitation as normalFeatures
- Tenant-level and workspace-level invitations
- Secure token-based authentication
- Pre-configured group assignments
- Email-linked invitations
- State tracking (created, sent, accepted, declined)
- Manual send trigger
- Public accept/decline endpoints
- Token-based invitation lookup
- Custom attributes support
Best Practices
Invitation Creation
- Always specify at least one group (tenant or workspace)
- Use valid email addresses for invitations
- Set appropriate groups based on user role
- Include workspace context when inviting to specific projects
Email Handling
- Implement email sending in your application
- Include clear invitation instructions in email
- Provide direct links with token embedded
- Set reasonable email templates
Token Security
- Tokens are automatically generated (12 characters)
- Never expose tokens in logs
- Tokens are single-use (invitation can only be accepted once)
- Validate tokens before accepting invitations
State Management
- Check invitation state before sending
- Don't accept already-accepted invitations
- Delete declined invitations if no longer needed
- Track sent invitations for follow-up
Troubleshooting
"Invitation token does not match"
Cause: Token provided doesn't match the invitation's stored token.
Solution: Ensure the correct token is used. Retrieve invitation by token first:
qry := &query.InvitationQueryGetByToken{Token: "provided-token"}Group validation fails during creation
Cause: Specified group UUIDs don't exist in the tenant/workspace.
Solution:
- Verify tenant groups exist in the tenant
- Verify workspace groups exist in the workspace
- Ensure groups are created before creating invitation
Invitation not sending email
Cause: Email sending is not implemented or configured.
Solution: Implement a reactor that listens to InvitationSentEvent:
// reactor/send_email.go
func (r *Reactor) OnInvitationSent(evt InvitationSentEvent) {
// Send email with invitation link
sendEmail(evt.Email, generateInvitationLink(evt.Token))
}User can't accept invitation
Check:
- Is the invitation state "sent"?
- Is the token correct?
- Does the user have a valid account?
- Has the invitation already been accepted?
Identity not created after acceptance
Cause: Invitation acceptance didn't trigger identity creation.
Solution: Implement a reactor that listens to InvitationAcceptedEvent:
func (r *Reactor) OnInvitationAccepted(evt InvitationAcceptedEvent) {
// Create identity with invitation's groups
cmd := &identity.IdentityCommandCreate{
IdentityUuid: evt.TargetIdentityUuid,
AccountUuid: evt.AccountUuid,
GroupUuids: evt.GroupUuids,
}
// Add to workspace if workspace invitation
if len(evt.WorkspaceUuid) > 0 {
// Add workspace membership
}
}Event-Driven Automation
Reactors
The Invitation domain uses reactors for automation:
Auto-Send Email on Creation
// Reactor listens to InvitationCreatedEvent
func (r *Reactor) OnInvitationCreated(evt InvitationCreatedEvent) {
// Automatically send invitation email
sendInvitationCommand := &command.InvitationCommandSend{
InvitationUuid: evt.InvitationUuid,
}
}Create Identity on Acceptance
// Reactor listens to InvitationAcceptedEvent
func (r *Reactor) OnInvitationAccepted(evt InvitationAcceptedEvent) {
// Create identity with invitation's configuration
createIdentityCommand := &identity.IdentityCommandCreate{
IdentityUuid: evt.TargetIdentityUuid,
AccountUuid: evt.AccountUuid,
GroupUuids: evt.GroupUuids,
}
// If workspace invitation, add to workspace
if len(evt.WorkspaceUuid) > 0 {
addMemberCommand := &workspace.WorkspaceCommandAddMember{
WorkspaceUuid: evt.WorkspaceUuid,
IdentityUuid: evt.TargetIdentityUuid,
GroupUuids: evt.WorkspaceGroupUuids,
}
}
}Integration with Other Domains
Identity Domain
When invitation is accepted:
// Create identity with invitation's groups
cmd := &identity.IdentityCommandCreate{
IdentityUuid: invitation.TargetIdentityUuid,
AccountUuid: invitation.AccountUuid,
GroupUuids: invitation.GroupUuids,
}Workspace Domain
For workspace invitations:
// Add member to workspace with workspace groups
cmd := &workspace.WorkspaceCommandAddMember{
WorkspaceUuid: invitation.WorkspaceUuid,
IdentityUuid: invitation.TargetIdentityUuid,
GroupUuids: invitation.WorkspaceGroupUuids,
}Account Domain
Integration with registration:
// During account registration, check for invitations
invitations := findInvitationsByEmail(registeredEmail)
for _, invitation := range invitations {
// Auto-accept or prompt user to accept
}Group Domain
Invitations reference groups:
- Validate groups exist before creating invitation
- Groups must exist in correct context (tenant/workspace)
Security Considerations
Token Generation
- Tokens are randomly generated (12 characters)
- Cryptographically secure random generation
- Tokens are unique per invitation
- Never reuse tokens
Email Validation
- Always validate email format
- Check for existing identities with same email
- Prevent duplicate invitations to same email
Authorization
- Only authorized users can create invitations
- Workspace invitations require workspace membership
- Accept/Decline endpoints are public (token-based security)
State Transitions
Valid Transitions:
- created → sent
- sent → accepted
- sent → declined
Invalid Transitions:
- accepted → sent (can't resend accepted invitation)
- declined → accepted (can't accept declined invitation)Token Format
Invitations use a 12-character alphanumeric token:
Example: abc123xyz789
Properties:
- Length: 12 characters
- Character set:
a-zA-Z0-9 - Collision resistance: Very high (62^12 combinations)
- Human-readable: Can be typed if needed
Commands
- InvitationCommandAccept
- InvitationCommandCreate
- InvitationCommandDecline
- InvitationCommandRemove
- InvitationCommandRemoveAttribute
- InvitationCommandSend
- InvitationCommandSetAttribute
- InvitationCommandUpdate
InvitationCommandAccept
Domain Command Struct:
type InvitationCommandAccept struct {
InvitationUuid string `json:"invitationUuid"`
AccountUuid string `json:"accountUuid"`
TargetIdentityUuid string `json:"targetIdentityUuid,omitempty"`
Token string `json:"token"`
}Domain Command Handling Method:
func (cs *commandHandler) InvitationCommandAccept(ctx context.Context, cmd comby.Command, domainCmd *InvitationCommandAccept) ([]comby.Event, error)InvitationCommandCreate
Domain Command Struct:
type InvitationCommandCreate struct {
InvitationUuid string `json:"invitationUuid"`
IdentityUuid string `json:"identityUuid"`
GroupUuids []string `json:"groupUuids,omitempty"` // Tenant-level groups
WorkspaceUuid string `json:"workspaceUuid,omitempty"` // Optional workspace UUID for workspace-scoped invitations
WorkspaceGroupUuids []string `json:"workspaceGroupUuids,omitempty"` // Workspace-level groups (requires workspace context)
Email string `json:"email"`
Attributes string `json:"attributes,omitempty"`
}Domain Command Handling Method:
func (cs *commandHandler) InvitationCommandCreate(ctx context.Context, cmd comby.Command, domainCmd *InvitationCommandCreate) ([]comby.Event, error)InvitationCommandDecline
Domain Command Struct:
type InvitationCommandDecline struct {
InvitationUuid string `json:"invitationUuid"`
AccountUuid string `json:"accountUuid"`
Token string `json:"token"`
}Domain Command Handling Method:
func (cs *commandHandler) InvitationCommandDecline(ctx context.Context, cmd comby.Command, domainCmd *InvitationCommandDecline) ([]comby.Event, error)InvitationCommandRemove
Domain Command Struct:
type InvitationCommandRemove struct {
InvitationUuid string `json:"invitationUuid"`
}Domain Command Handling Method:
func (cs *commandHandler) InvitationCommandRemove(ctx context.Context, cmd comby.Command, domainCmd *InvitationCommandRemove) ([]comby.Event, error)InvitationCommandRemoveAttribute
Domain Command Struct:
type InvitationCommandRemoveAttribute struct {
InvitationUuid string `json:"invitationUuid"`
Key string `json:"key"`
}Domain Command Handling Method:
func (cs *commandHandler) InvitationCommandRemoveAttribute(ctx context.Context, cmd comby.Command, domainCmd *InvitationCommandRemoveAttribute) ([]comby.Event, error)InvitationCommandSend
Domain Command Struct:
type InvitationCommandSend struct {
InvitationUuid string `json:"invitationUuid"`
}Domain Command Handling Method:
func (cs *commandHandler) InvitationCommandSend(ctx context.Context, cmd comby.Command, domainCmd *InvitationCommandSend) ([]comby.Event, error)InvitationCommandSetAttribute
Domain Command Struct:
type InvitationCommandSetAttribute struct {
InvitationUuid string `json:"invitationUuid"`
Key string `json:"key"`
Value any `json:"value"`
}Domain Command Handling Method:
func (cs *commandHandler) InvitationCommandSetAttribute(ctx context.Context, cmd comby.Command, domainCmd *InvitationCommandSetAttribute) ([]comby.Event, error)InvitationCommandUpdate
Domain Command Struct:
type InvitationCommandUpdate struct {
InvitationUuid string `json:"invitationUuid"`
Attributes string `json:"attributes,omitempty"`
PatchedFields []string `json:"patchedFields"`
}Domain Command Handling Method:
func (cs *commandHandler) InvitationCommandUpdate(ctx context.Context, cmd comby.Command, domainCmd *InvitationCommandUpdate) ([]comby.Event, error)Queries
Domain Query Structs:
Domain Query Responses:
InvitationQueryList
Domain Query Struct:
type InvitationQueryList struct {
TenantUuid string `json:"tenantUuid"`
Page int64 `json:"page,omitempty"`
PageSize int64 `json:"pageSize,omitempty"`
OrderBy string `json:"orderBy,omitempty"`
Attributes string `json:"attributes,omitempty"`
IncludeHistory bool `json:"includeHistory,omitempty"`
}Domain Query Handling Method:
func (qs *queryHandler) InvitationQueryList(ctx context.Context, qry comby.Query, domainQry *InvitationQueryList) (*InvitationQueryListResponse, error)InvitationQueryModel
Domain Query Struct:
type InvitationQueryModel struct {
InvitationUuid string `json:"invitationUuid"`
IncludeHistory bool `json:"includeHistory,omitempty"`
}Domain Query Handling Method:
func (qs *queryHandler) InvitationQueryModel(ctx context.Context, qry comby.Query, domainQry *InvitationQueryModel) (*InvitationQueryItemResponse, error)InvitationQueryModelByInvitationToken
Domain Query Struct:
type InvitationQueryModelByInvitationToken struct {
InvitationToken string `json:"invitationToken"`
}Domain Query Handling Method:
func (qs *queryHandler) InvitationQueryModelByInvitationToken(ctx context.Context, qry comby.Query, domainQry *InvitationQueryModelByInvitationToken) (*InvitationQueryItemResponse, error)InvitationQueryListResponse
type InvitationQueryListResponse struct {
Items []*readmodel.InvitationModel `json:"items,omitempty"`
Total int64 `json:"total,omitempty"`
Page int64 `json:"page,omitempty"`
PageSize int64 `json:"pageSize,omitempty"`
}InvitationQueryItemResponse
type InvitationQueryItemResponse struct {
Item *readmodel.InvitationModel `json:"item,omitempty"`
}Events
- InvitationAcceptedEvent
- InvitationCreatedEvent
- InvitationDeclinedEvent
- InvitationRemovedEvent
- InvitationAttributeRemovedEvent
- InvitationSentEvent
- InvitationAttributeSetEvent
- InvitationUpdatedEvent
InvitationAcceptedEvent
Domain Event Struct:
type InvitationAcceptedEvent struct {
AccountUuid string `json:"accountUuid"`
TargetIdentityUuid string `json:"targetIdentityUuid,omitempty"` // if identity already exists
}Domain Event Handling Method:
func (agg *Invitation) InvitationAcceptedEvent(ctx context.Context, evt comby.Event, domainEvt *InvitationAcceptedEvent) (error)InvitationCreatedEvent
Domain Event Struct:
type InvitationCreatedEvent struct {
IdentityUuid string `json:"identityUuid"`
GroupUuids []string `json:"groupUuids,omitempty"` // Tenant-level groups
WorkspaceGroupUuids []string `json:"workspaceGroupUuids,omitempty"` // Workspace-level groups
WorkspaceUuid string `json:"workspaceUuid,omitempty"` // Optional workspace UUID for workspace-scoped invitations
Token string `json:"token"`
Email string `json:"email"`
Attributes string `json:"attributes,omitempty"`
}Domain Event Handling Method:
func (agg *Invitation) InvitationCreatedEvent(ctx context.Context, evt comby.Event, domainEvt *InvitationCreatedEvent) (error)InvitationDeclinedEvent
Domain Event Struct:
type InvitationDeclinedEvent struct {
AccountUuid string `json:"accountUuid"`
}Domain Event Handling Method:
func (agg *Invitation) InvitationDeclinedEvent(ctx context.Context, evt comby.Event, domainEvt *InvitationDeclinedEvent) (error)InvitationRemovedEvent
Domain Event Struct:
type InvitationRemovedEvent struct {
Reason string `json:"reason,omitempty"`
}Domain Event Handling Method:
func (agg *Invitation) InvitationRemovedEvent(ctx context.Context, evt comby.Event, domainEvt *InvitationRemovedEvent) (error)InvitationAttributeRemovedEvent
Domain Event Struct:
type InvitationAttributeRemovedEvent struct {
Key string `json:"key"`
}Domain Event Handling Method:
func (agg *Invitation) InvitationAttributeRemovedEvent(ctx context.Context, evt comby.Event, domainEvt *InvitationAttributeRemovedEvent) (error)InvitationSentEvent
Domain Event Struct:
type InvitationSentEvent struct {
State string `json:"state"`
}Domain Event Handling Method:
func (agg *Invitation) InvitationSentEvent(ctx context.Context, evt comby.Event, domainEvt *InvitationSentEvent) (error)InvitationAttributeSetEvent
Domain Event Struct:
type InvitationAttributeSetEvent struct {
Key string `json:"key"`
Value any `json:"value"`
}Domain Event Handling Method:
func (agg *Invitation) InvitationAttributeSetEvent(ctx context.Context, evt comby.Event, domainEvt *InvitationAttributeSetEvent) (error)InvitationUpdatedEvent
Domain Event Struct:
type InvitationUpdatedEvent struct {
Attributes string `json:"attributes,omitempty"`
}Domain Event Handling Method:
func (agg *Invitation) InvitationUpdatedEvent(ctx context.Context, evt comby.Event, domainEvt *InvitationUpdatedEvent) (error)Aggregate
Aggregate Struct:
type Invitation struct {
*comby.BaseAggregate
// GroupUuids groups to which the invited account will be added (tenant-level)
GroupUuids []string
// WorkspaceUuid - when set, this invitation is for a specific workspace
WorkspaceUuid string
// WorkspaceGroupUuids groups to which the invited account will be added (workspace-level)
Only used when WorkspaceUuid is set
WorkspaceGroupUuids []string
// IdentityUuid who created the invitation
IdentityUuid string
// AccountUuid who accepted the invitation
AccountUuid string
// Value Objects
TargetIdentityUuid string
Token string
Email string
State string
}Methods
Accept
func (agg *Invitation) Accept(opts ) (error)Create
func (agg *Invitation) Create(opts ) (error)Decline
func (agg *Invitation) Decline(opts ) (error)Remove
func (agg *Invitation) Remove(opts ) (error)RemoveAttribute
func (agg *Invitation) RemoveAttribute(opts ) (error)Send
func (agg *Invitation) Send(opts ) (error)SetAttribute
func (agg *Invitation) SetAttribute(opts ) (error)Update
func (agg *Invitation) Update(opts ) (error)Event Handlers
Reactor
| Domain Event | Method |
|---|---|
aggregateInvitation.InvitationAcceptedEvent | InvitationAcceptedEvent |
aggregateInvitation.InvitationCreatedEvent | InvitationCreatedEvent |
InvitationReadmodel
| Domain Event | Method |
|---|---|
workspaceAggregate.WorkspaceUpdatedEvent | WorkspaceUpdatedEvent |
workspaceAggregate.WorkspaceRemovedEvent | WorkspaceRemovedEvent |
workspaceAggregate.WorkspaceCreatedEvent | WorkspaceCreatedEvent |
tenantAggregate.TenantCreatedEvent | TenantCreatedEvent |
tenantAggregate.TenantUpdatedEvent | TenantUpdatedEvent |
tenantAggregate.TenantRemovedEvent | TenantRemovedEvent |
aggregate.InvitationAcceptedEvent | InvitationAcceptedEvent |
aggregate.InvitationAttributeRemovedEvent | InvitationAttributeRemovedEvent |
aggregate.InvitationUpdatedEvent | InvitationUpdatedEvent |
aggregate.InvitationRemovedEvent | InvitationRemovedEvent |
aggregate.InvitationCreatedEvent | InvitationCreatedEvent |
aggregate.InvitationDeclinedEvent | InvitationDeclinedEvent |
aggregate.InvitationSentEvent | InvitationSentEvent |
aggregate.InvitationAttributeSetEvent | InvitationAttributeSetEvent |
identityAggregate.IdentityRemovedEvent | IdentityRemovedEvent |
identityAggregate.IdentityProfileUpdatedEvent | IdentityProfileUpdatedEvent |
identityAggregate.IdentityCreatedEvent | IdentityCreatedEvent |
groupAggregate.GroupUpdatedEvent | GroupUpdatedEvent |
groupAggregate.GroupRemovedEvent | GroupRemovedEvent |
groupAggregate.GroupAddedEvent | GroupAddedEvent |
accountAggregate.AccountCredentialsUpdatedEvent | AccountCredentialsUpdatedEvent |