Skip to content

Group

Overview

The Group domain in comby is designed to manage groups within a tenant. Groups act as organizational units that can hold runtime permissions and serve as collections for identities. Each group belongs to a specific tenant and provides a structured way to manage permissions and membership. The domain is implemented through the Group aggregate, which models groups, handles domain events, and enforces business rules.

Concept

What is a Group?

A Group is a named collection of permissions within a tenant. Instead of assigning permissions directly to each identity, you assign identities to groups. Each group defines a role with specific permissions that all its members inherit.

Key Characteristics:

  • Tenant-Scoped: Each group belongs to a specific tenant
  • Permission Collections: Groups hold multiple permissions as a list
  • Reusable Roles: Same group can be assigned to multiple identities
  • Named & Described: Human-readable names and descriptions
  • System Groups: Special protected groups (e.g., system admins)
Tenant
└── Groups
    ├── Administrators (permissions: all operations)
    ├── Developers (permissions: Customer.Create, Customer.Update, Invoice.Create, Invoice.Update)
    ├── Viewers (permissions: Customer.List, Customer.Get, Invoice.List, Invoice.Get)
    └── Custom Roles (permissions: Custom.defined)

Architecture

Hierarchy

System
└── Tenant
    └── Groups
        ├── Name
        ├── Description
        ├── Permissions []string
        └── Attributes

Group is connected to:

  • Tenant: Each group belongs to exactly one tenant
  • Identity: Identities are assigned to groups to inherit permissions
  • Workspace: Workspaces can have their own groups (separate domain)
  • Auth: Auth domain consumes group events for authorization

Domain Model

A Group always references only one Tenant; it is neither possible nor intended for a Group to be shared across multiple Tenants. In the comby default setup, the system Tenant always includes a System Group (named "system-admin"). This System Group is implemented with full rights across the system.

If an identity in the system Tenant is assigned to this Group, it can perform all actions and access all permissions within the system. This is also the reason why the System Group cannot be deleted.

Permission Format

Permissions are defined as strings with the format:

{Domain}.{Operation}

Examples:

  • Customer.Create - Create customers
  • Customer.Update - Update customers
  • Customer.List - List customers
  • Customer.Get - Get customer details
  • Customer.Remove - Remove customers

Common Patterns:

Domain Operations:
- Customer.Create, Customer.Update, Customer.Remove, Customer.List, Customer.Get
- Invoice.Create, Invoice.Update, Invoice.Remove, Invoice.List, Invoice.Get
- Tenant.Create, Tenant.Update, Tenant.Remove

System Groups

Certain groups are protected system groups:

SYSTEM_TENANT_GROUP_ADMIN

  • UUID: Special system UUID
  • Permissions: All operations across all domains
  • Cannot be renamed or deleted
  • Members bypass all authorization checks
  • Only exists in SYSTEM_TENANT

Structure

The structure of a Group is simple and consists of the following elements:

  • Name: The name of the group, serving as a human-readable identifier.
  • Description: Additional context or information about the group.
  • Attributes: A map for storing additional metadata associated with the group.
  • Permissions: A list of runtime permissions assigned to the group, defining its access and capabilities.

Permissions are simple strings that can, for example, be assigned to a Group through the Admin Dashboard. comby automatically generates permissions for all commands and queries, ensuring seamless integration. If needed, these permissions can be overridden with custom logic. Example: Allowing user logins for anonymous users.

Use Cases

1. Standard RBAC Roles

Tenant: "Acme Corp"
├── Group: "Administrators"
│   └── Permissions: [all domain operations]
├── Group: "Developers"
│   └── Permissions: ["Customer.Create", "Customer.Update", "Customer.Remove", "Customer.List", "Customer.Get",
│                      "Invoice.Create", "Invoice.Update", "Invoice.Remove", "Invoice.List", "Invoice.Get"]
├── Group: "Support Team"
│   └── Permissions: ["Customer.Get", "Customer.List", "Customer.Update"]
└── Group: "Viewers"
    └── Permissions: ["Customer.List", "Customer.Get", "Invoice.List", "Invoice.Get"]

2. Department-Based Access

Tenant: "Enterprise Inc"
├── Group: "Finance Department"
│   └── Permissions: ["Invoice.Create", "Invoice.Update", "Invoice.Remove", "Invoice.List", "Invoice.Get",
│                      "Payment.Create", "Payment.Update", "Payment.Remove", "Payment.List", "Payment.Get",
│                      "Customer.List"]
├── Group: "Sales Department"
│   └── Permissions: ["Customer.Create", "Customer.Update", "Customer.Remove", "Customer.List", "Customer.Get",
│                      "Deal.Create", "Deal.Update", "Deal.Remove", "Deal.List", "Deal.Get",
│                      "Invoice.Create"]
└── Group: "Operations"
    └── Permissions: ["Order.Create", "Order.Update", "Order.Remove", "Order.List", "Order.Get",
                       "Inventory.Create", "Inventory.Update", "Inventory.Remove", "Inventory.List", "Inventory.Get",
                       "Customer.Get"]

3. Multi-Level Access

Tenant: "SaaS Platform"
├── Group: "Tier 1 Support"
│   └── Permissions: ["Ticket.Get", "Ticket.List", "Ticket.Update"]
├── Group: "Tier 2 Support"
│   └── Permissions: ["Ticket.Create", "Ticket.Update", "Ticket.Remove", "Ticket.List", "Ticket.Get",
│                      "Customer.Get", "Customer.Update"]
└── Group: "Support Managers"
    └── Permissions: ["Ticket.Create", "Ticket.Update", "Ticket.Remove", "Ticket.List", "Ticket.Get",
                       "Customer.Create", "Customer.Update", "Customer.Remove", "Customer.List", "Customer.Get",
                       "Report.Create", "Report.Update", "Report.Remove", "Report.List", "Report.Get"]

4. Feature-Based Permissions

Tenant: "Product Platform"
├── Group: "Basic Users"
│   └── Permissions: ["Dashboard.view", "Profile.Update"]
├── Group: "Premium Users"
│   └── Permissions: ["Dashboard.view", "Profile.Update", "Analytics.view", "Export.Create"]
└── Group: "Enterprise Users"
    └── Permissions: [all domain operations]

Features

  • Tenant-scoped groups
  • Flexible permission assignments
  • Explicit permission definitions
  • System admin groups
  • Group name uniqueness per tenant
  • Protected system groups
  • Custom attributes support
  • Batch permission updates
  • Query by name

Best Practices

Group Design

  • Use descriptive, role-based group names (e.g., "Developers", "Administrators")
  • Create groups based on job functions, not individuals
  • Keep the number of groups manageable (5-15 per tenant)
  • Document group purposes in descriptions

Permission Assignment

  • Follow principle of least privilege
  • Grant only specific permissions needed for the role
  • Be explicit about each permission granted
  • Review and audit permissions regularly

Group Naming

  • Use clear, consistent naming conventions
  • Avoid abbreviations that aren't obvious
  • Include context in names when helpful (e.g., "Sales-Manager" vs "Manager")

Good Examples:

  • "Administrators", "Developers", "Support-Team"
  • "Finance-Department", "Sales-Representatives"
  • "Read-Only-Users", "Power-Users"

Bad Examples:

  • "Group1", "G1", "Admin123"
  • "Dev", "Usr", "Tmp"

Permission Management

  • Use PatchedFields for partial updates
  • Test permission changes in non-production first
  • Keep permission lists organized and sorted
  • Group related permissions together for clarity

Security

  • Limit full permissions to system administrators only
  • Regularly audit who has which permissions
  • Remove unused groups promptly
  • Monitor group membership changes

Troubleshooting

"Group already exists" error

Cause: A group with the same name already exists in the tenant.

Solution: Group names must be unique within a tenant. Choose a different name or delete the existing group first.

Cannot rename system group

Cause: Attempting to rename a protected system group (e.g., SYSTEM_TENANT_GROUP_ADMIN).

Solution: System groups cannot be renamed. Create a new custom group instead.

Permission not working

Check:

  1. Is the permission format correct? (domain.operation)
  2. Is the permission actually assigned to the group?
  3. Is the identity a member of the group?
  4. Is the Auth domain properly registered?

Debug:

go
// Check group permissions
group, _ := GetGroupReadmodel().GetModel("group-uuid")
fmt.Printf("Permissions: %v\n", group.Permissions)

// Check identity's groups via Auth readmodel
identityCtx, _ := auth.GetAuthReadmodel().GetIdentity("identity-uuid")
for _, group := range identityCtx.Groups {
    fmt.Printf("Group: %s, Permissions: %v\n", group.Name, group.PermissionNames)
}

Group deletion fails

Possible causes:

  1. Group still has members assigned
  2. Group is a system group
  3. Group doesn't exist

Solution: Remove all identities from the group before deletion, unless it's a system group which cannot be deleted.

Integration with Other Domains

Identity Domain

Identities are assigned to groups via the Identity domain:

go
// Add identity to group
cmd, _ := comby.NewCommand("Identity", &command.IdentityCommandAddGroup{
    IdentityUuid: "identity-uuid",
    GroupUuid:    "group-uuid",
})

Auth Domain

The Auth domain consumes group events to build authorization context:

  • Groups are loaded into the auth readmodel
  • Permissions are evaluated during command execution
  • Group changes are reflected immediately in authorization

Workspace Domain

Workspaces have their own separate group system:

  • Workspace groups are independent from tenant groups
  • Workspace groups only apply within that workspace
  • Additive model: Tenant OR workspace permissions grant access

Commands

GroupCommandCreate

Domain Command Struct:

go
type GroupCommandCreate struct {
	GroupUuid   string   `json:"groupUuid"`
	Name        string   `json:"name"`
	Description string   `json:"description,omitempty"`
	Attributes  string   `json:"attributes,omitempty"`
	Permissions []string `json:"permissions,omitempty"`
}

Domain Command Handling Method:

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

GroupCommandRemove

Domain Command Struct:

go
type GroupCommandRemove struct {
	GroupUuid string `json:"groupUuid"`
}

Domain Command Handling Method:

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

GroupCommandRemoveAttribute

Domain Command Struct:

go
type GroupCommandRemoveAttribute struct {
	GroupUuid string `json:"groupUuid"`
	Key       string `json:"key"`
}

Domain Command Handling Method:

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

GroupCommandSetAttribute

Domain Command Struct:

go
type GroupCommandSetAttribute struct {
	GroupUuid string `json:"groupUuid"`
	Key       string `json:"key"`
	Value     any    `json:"value"`
}

Domain Command Handling Method:

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

GroupCommandUpdate

Domain Command Struct:

go
type GroupCommandUpdate struct {
	GroupUuid     string   `json:"groupUuid"`
	Name          string   `json:"name,omitempty"`
	Description   string   `json:"description,omitempty"`
	Attributes    string   `json:"attributes,omitempty"`
	Permissions   []string `json:"permissions,omitempty"`
	PatchedFields []string `json:"patchedFields" doc:"list of fields that should be patched - comma separated" example:"field1,field2"`
}

Domain Command Handling Method:

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

Queries

Domain Query Structs:

Domain Query Responses:

GroupQueryList

Domain Query Struct:

go
type GroupQueryList 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) GroupQueryList(ctx context.Context, qry comby.Query, domainQry *GroupQueryList) (*GroupQueryListResponse, error)

GroupQueryModelByName

Domain Query Struct:

go
type GroupQueryModelByName struct {
	Name string `json:"name"`
}

Domain Query Handling Method:

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

GroupQueryModel

Domain Query Struct:

go
type GroupQueryModel struct {
	GroupUuid      string `json:"groupUuid"`
	IncludeHistory bool   `json:"includeHistory,omitempty"`
}

Domain Query Handling Method:

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

GroupQueryListResponse

go
type GroupQueryListResponse struct {
	Items    []*readmodel.GroupModel `json:"items,omitempty"`
	Total    int64                   `json:"total,omitempty"`
	Page     int64                   `json:"page,omitempty"`
	PageSize int64                   `json:"pageSize,omitempty"`
}

GroupQueryItemResponse

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

Events

GroupAddedEvent

Domain Event Struct:

go
type GroupAddedEvent struct {
	Name        string   `json:"name"`
	Description string   `json:"description,omitempty"`
	Attributes  string   `json:"attributes,omitempty"`
	Permissions []string `json:"permissions,omitempty"`
}

Domain Event Handling Method:

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

GroupRemovedEvent

Domain Event Struct:

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

Domain Event Handling Method:

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

GroupAttributeRemovedEvent

Domain Event Struct:

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

Domain Event Handling Method:

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

GroupAttributeSetEvent

Domain Event Struct:

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

Domain Event Handling Method:

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

GroupUpdatedEvent

Domain Event Struct:

go
type GroupUpdatedEvent struct {
	Name        string   `json:"name"`
	Description string   `json:"description"`
	Attributes  string   `json:"attributes,omitempty"`
	Permissions []string `json:"permissions,omitempty"`
}

Domain Event Handling Method:

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

Aggregate

Aggregate Struct:

go
type Group struct {
	*comby.BaseAggregate
	// Value Objects
	Permissions []string
	Name        string
	Description string
}

Methods

Add

go
func (agg *Group) Add(opts ) (error)

Remove

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

RemoveAttribute

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

SetAttribute

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

Update

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

Event Handlers

GroupReadmodel

Domain EventMethod
tenantAggregate.TenantCreatedEventTenantCreatedEvent
tenantAggregate.TenantRemovedEventTenantRemovedEvent
tenantAggregate.TenantUpdatedEventTenantUpdatedEvent
groupAggregate.GroupAddedEventGroupAddedEvent
groupAggregate.GroupUpdatedEventGroupUpdatedEvent
groupAggregate.GroupRemovedEventGroupRemovedEvent
groupAggregate.GroupAttributeSetEventGroupAttributeSetEvent
groupAggregate.GroupAttributeRemovedEventGroupAttributeRemovedEvent

Custom Permissions

NameTypeComment
GroupCommandCreateCommandCreate new group
GroupCommandUpdateCommandUpdate existing group
GroupCommandRemoveCommandRemove existing group
GroupCommandSetAttributeCommandSet single attribute of existing group
GroupCommandRemoveAttributeCommandRemove single attribute from existing group
GroupQueryListQueryList groups
GroupQueryModelQueryGet group by id
GroupQueryModelByNameQueryGet group by name