Skip to content

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 Created

Architecture

Hierarchy

System
└── Tenant
    ├── Invitations (Tenant-Level)
    │   ├── Token
    │   ├── Email
    │   ├── GroupUuids (Tenant groups)
    │   └── State
    └── Workspaces
        └── Invitations (Workspace-Level)
            ├── Token
            ├── Email
            ├── GroupUuids (Tenant groups)
            ├── WorkspaceGroupUuids (Workspace groups)
            └── State

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

StateDescription
createdInvitation created but not yet sent
sentInvitation sent to recipient (email)
acceptedUser accepted the invitation
declinedUser 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 groups

2. 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 workspace

Structure

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 group

2. 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 permissions

3. 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 accepted

4. 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 normal

Features

  • 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:

go
qry := &query.InvitationQueryGetByToken{Token: "provided-token"}

Group validation fails during creation

Cause: Specified group UUIDs don't exist in the tenant/workspace.

Solution:

  1. Verify tenant groups exist in the tenant
  2. Verify workspace groups exist in the workspace
  3. 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:

go
// 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:

  1. Is the invitation state "sent"?
  2. Is the token correct?
  3. Does the user have a valid account?
  4. 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:

go
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

go
// Reactor listens to InvitationCreatedEvent
func (r *Reactor) OnInvitationCreated(evt InvitationCreatedEvent) {
    // Automatically send invitation email
    sendInvitationCommand := &command.InvitationCommandSend{
        InvitationUuid: evt.InvitationUuid,
    }
}

Create Identity on Acceptance

go
// 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:

go
// Create identity with invitation's groups
cmd := &identity.IdentityCommandCreate{
    IdentityUuid: invitation.TargetIdentityUuid,
    AccountUuid:  invitation.AccountUuid,
    GroupUuids:   invitation.GroupUuids,
}

Workspace Domain

For workspace invitations:

go
// Add member to workspace with workspace groups
cmd := &workspace.WorkspaceCommandAddMember{
    WorkspaceUuid: invitation.WorkspaceUuid,
    IdentityUuid:  invitation.TargetIdentityUuid,
    GroupUuids:    invitation.WorkspaceGroupUuids,
}

Account Domain

Integration with registration:

go
// 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

Domain Command Struct:

go
type InvitationCommandAccept struct {
	InvitationUuid     string `json:"invitationUuid"`
	AccountUuid        string `json:"accountUuid"`
	TargetIdentityUuid string `json:"targetIdentityUuid,omitempty"`
	Token              string `json:"token"`
}

Domain Command Handling Method:

go
func (cs *commandHandler) InvitationCommandAccept(ctx context.Context, cmd comby.Command, domainCmd *InvitationCommandAccept) ([]comby.Event, error)

InvitationCommandCreate

Domain Command Struct:

go
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:

go
func (cs *commandHandler) InvitationCommandCreate(ctx context.Context, cmd comby.Command, domainCmd *InvitationCommandCreate) ([]comby.Event, error)

InvitationCommandDecline

Domain Command Struct:

go
type InvitationCommandDecline struct {
	InvitationUuid string `json:"invitationUuid"`
	AccountUuid    string `json:"accountUuid"`
	Token          string `json:"token"`
}

Domain Command Handling Method:

go
func (cs *commandHandler) InvitationCommandDecline(ctx context.Context, cmd comby.Command, domainCmd *InvitationCommandDecline) ([]comby.Event, error)

InvitationCommandRemove

Domain Command Struct:

go
type InvitationCommandRemove struct {
	InvitationUuid string `json:"invitationUuid"`
}

Domain Command Handling Method:

go
func (cs *commandHandler) InvitationCommandRemove(ctx context.Context, cmd comby.Command, domainCmd *InvitationCommandRemove) ([]comby.Event, error)

InvitationCommandRemoveAttribute

Domain Command Struct:

go
type InvitationCommandRemoveAttribute struct {
	InvitationUuid string `json:"invitationUuid"`
	Key            string `json:"key"`
}

Domain Command Handling Method:

go
func (cs *commandHandler) InvitationCommandRemoveAttribute(ctx context.Context, cmd comby.Command, domainCmd *InvitationCommandRemoveAttribute) ([]comby.Event, error)

InvitationCommandSend

Domain Command Struct:

go
type InvitationCommandSend struct {
	InvitationUuid string `json:"invitationUuid"`
}

Domain Command Handling Method:

go
func (cs *commandHandler) InvitationCommandSend(ctx context.Context, cmd comby.Command, domainCmd *InvitationCommandSend) ([]comby.Event, error)

InvitationCommandSetAttribute

Domain Command Struct:

go
type InvitationCommandSetAttribute struct {
	InvitationUuid string `json:"invitationUuid"`
	Key            string `json:"key"`
	Value          any    `json:"value"`
}

Domain Command Handling Method:

go
func (cs *commandHandler) InvitationCommandSetAttribute(ctx context.Context, cmd comby.Command, domainCmd *InvitationCommandSetAttribute) ([]comby.Event, error)

InvitationCommandUpdate

Domain Command Struct:

go
type InvitationCommandUpdate struct {
	InvitationUuid string   `json:"invitationUuid"`
	Attributes     string   `json:"attributes,omitempty"`
	PatchedFields  []string `json:"patchedFields"`
}

Domain Command Handling Method:

go
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:

go
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:

go
func (qs *queryHandler) InvitationQueryList(ctx context.Context, qry comby.Query, domainQry *InvitationQueryList) (*InvitationQueryListResponse, error)

InvitationQueryModel

Domain Query Struct:

go
type InvitationQueryModel struct {
	InvitationUuid string `json:"invitationUuid"`
	IncludeHistory bool   `json:"includeHistory,omitempty"`
}

Domain Query Handling Method:

go
func (qs *queryHandler) InvitationQueryModel(ctx context.Context, qry comby.Query, domainQry *InvitationQueryModel) (*InvitationQueryItemResponse, error)

InvitationQueryModelByInvitationToken

Domain Query Struct:

go
type InvitationQueryModelByInvitationToken struct {
	InvitationToken string `json:"invitationToken"`
}

Domain Query Handling Method:

go
func (qs *queryHandler) InvitationQueryModelByInvitationToken(ctx context.Context, qry comby.Query, domainQry *InvitationQueryModelByInvitationToken) (*InvitationQueryItemResponse, error)

InvitationQueryListResponse

go
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

go
type InvitationQueryItemResponse struct {
	Item *readmodel.InvitationModel `json:"item,omitempty"`
}

Events

InvitationAcceptedEvent

Domain Event Struct:

go
type InvitationAcceptedEvent struct {
	AccountUuid        string `json:"accountUuid"`
	TargetIdentityUuid string `json:"targetIdentityUuid,omitempty"` // if identity already exists
}

Domain Event Handling Method:

go
func (agg *Invitation) InvitationAcceptedEvent(ctx context.Context, evt comby.Event, domainEvt *InvitationAcceptedEvent) (error)

InvitationCreatedEvent

Domain Event Struct:

go
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:

go
func (agg *Invitation) InvitationCreatedEvent(ctx context.Context, evt comby.Event, domainEvt *InvitationCreatedEvent) (error)

InvitationDeclinedEvent

Domain Event Struct:

go
type InvitationDeclinedEvent struct {
	AccountUuid string `json:"accountUuid"`
}

Domain Event Handling Method:

go
func (agg *Invitation) InvitationDeclinedEvent(ctx context.Context, evt comby.Event, domainEvt *InvitationDeclinedEvent) (error)

InvitationRemovedEvent

Domain Event Struct:

go
type InvitationRemovedEvent struct {
	Reason string `json:"reason,omitempty"`
}

Domain Event Handling Method:

go
func (agg *Invitation) InvitationRemovedEvent(ctx context.Context, evt comby.Event, domainEvt *InvitationRemovedEvent) (error)

InvitationAttributeRemovedEvent

Domain Event Struct:

go
type InvitationAttributeRemovedEvent struct {
	Key string `json:"key"`
}

Domain Event Handling Method:

go
func (agg *Invitation) InvitationAttributeRemovedEvent(ctx context.Context, evt comby.Event, domainEvt *InvitationAttributeRemovedEvent) (error)

InvitationSentEvent

Domain Event Struct:

go
type InvitationSentEvent struct {
	State string `json:"state"`
}

Domain Event Handling Method:

go
func (agg *Invitation) InvitationSentEvent(ctx context.Context, evt comby.Event, domainEvt *InvitationSentEvent) (error)

InvitationAttributeSetEvent

Domain Event Struct:

go
type InvitationAttributeSetEvent struct {
	Key   string `json:"key"`
	Value any    `json:"value"`
}

Domain Event Handling Method:

go
func (agg *Invitation) InvitationAttributeSetEvent(ctx context.Context, evt comby.Event, domainEvt *InvitationAttributeSetEvent) (error)

InvitationUpdatedEvent

Domain Event Struct:

go
type InvitationUpdatedEvent struct {
	Attributes string `json:"attributes,omitempty"`
}

Domain Event Handling Method:

go
func (agg *Invitation) InvitationUpdatedEvent(ctx context.Context, evt comby.Event, domainEvt *InvitationUpdatedEvent) (error)

Aggregate

Aggregate Struct:

go
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

go
func (agg *Invitation) Accept(opts ) (error)

Create

go
func (agg *Invitation) Create(opts ) (error)

Decline

go
func (agg *Invitation) Decline(opts ) (error)

Remove

go
func (agg *Invitation) Remove(opts ) (error)

RemoveAttribute

go
func (agg *Invitation) RemoveAttribute(opts ) (error)

Send

go
func (agg *Invitation) Send(opts ) (error)

SetAttribute

go
func (agg *Invitation) SetAttribute(opts ) (error)

Update

go
func (agg *Invitation) Update(opts ) (error)

Event Handlers

Reactor

Domain EventMethod
aggregateInvitation.InvitationAcceptedEventInvitationAcceptedEvent
aggregateInvitation.InvitationCreatedEventInvitationCreatedEvent

InvitationReadmodel

Domain EventMethod
workspaceAggregate.WorkspaceUpdatedEventWorkspaceUpdatedEvent
workspaceAggregate.WorkspaceRemovedEventWorkspaceRemovedEvent
workspaceAggregate.WorkspaceCreatedEventWorkspaceCreatedEvent
tenantAggregate.TenantCreatedEventTenantCreatedEvent
tenantAggregate.TenantUpdatedEventTenantUpdatedEvent
tenantAggregate.TenantRemovedEventTenantRemovedEvent
aggregate.InvitationAcceptedEventInvitationAcceptedEvent
aggregate.InvitationAttributeRemovedEventInvitationAttributeRemovedEvent
aggregate.InvitationUpdatedEventInvitationUpdatedEvent
aggregate.InvitationRemovedEventInvitationRemovedEvent
aggregate.InvitationCreatedEventInvitationCreatedEvent
aggregate.InvitationDeclinedEventInvitationDeclinedEvent
aggregate.InvitationSentEventInvitationSentEvent
aggregate.InvitationAttributeSetEventInvitationAttributeSetEvent
identityAggregate.IdentityRemovedEventIdentityRemovedEvent
identityAggregate.IdentityProfileUpdatedEventIdentityProfileUpdatedEvent
identityAggregate.IdentityCreatedEventIdentityCreatedEvent
groupAggregate.GroupUpdatedEventGroupUpdatedEvent
groupAggregate.GroupRemovedEventGroupRemovedEvent
groupAggregate.GroupAddedEventGroupAddedEvent
accountAggregate.AccountCredentialsUpdatedEventAccountCredentialsUpdatedEvent