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
└── AttributesRelated Entities
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 customersCustomer.Update- Update customersCustomer.List- List customersCustomer.Get- Get customer detailsCustomer.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.RemoveSystem 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
PatchedFieldsfor 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:
- Is the permission format correct? (
domain.operation) - Is the permission actually assigned to the group?
- Is the identity a member of the group?
- Is the Auth domain properly registered?
Debug:
// 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:
- Group still has members assigned
- Group is a system group
- 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:
// 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
- GroupCommandRemove
- GroupCommandRemoveAttribute
- GroupCommandSetAttribute
- GroupCommandUpdate
GroupCommandCreate
Domain Command Struct:
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:
func (cs *commandHandler) GroupCommandCreate(ctx context.Context, cmd comby.Command, domainCmd *GroupCommandCreate) ([]comby.Event, error)GroupCommandRemove
Domain Command Struct:
type GroupCommandRemove struct {
GroupUuid string `json:"groupUuid"`
}Domain Command Handling Method:
func (cs *commandHandler) GroupCommandRemove(ctx context.Context, cmd comby.Command, domainCmd *GroupCommandRemove) ([]comby.Event, error)GroupCommandRemoveAttribute
Domain Command Struct:
type GroupCommandRemoveAttribute struct {
GroupUuid string `json:"groupUuid"`
Key string `json:"key"`
}Domain Command Handling Method:
func (cs *commandHandler) GroupCommandRemoveAttribute(ctx context.Context, cmd comby.Command, domainCmd *GroupCommandRemoveAttribute) ([]comby.Event, error)GroupCommandSetAttribute
Domain Command Struct:
type GroupCommandSetAttribute struct {
GroupUuid string `json:"groupUuid"`
Key string `json:"key"`
Value any `json:"value"`
}Domain Command Handling Method:
func (cs *commandHandler) GroupCommandSetAttribute(ctx context.Context, cmd comby.Command, domainCmd *GroupCommandSetAttribute) ([]comby.Event, error)GroupCommandUpdate
Domain Command Struct:
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:
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:
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:
func (qs *queryHandler) GroupQueryList(ctx context.Context, qry comby.Query, domainQry *GroupQueryList) (*GroupQueryListResponse, error)GroupQueryModelByName
Domain Query Struct:
type GroupQueryModelByName struct {
Name string `json:"name"`
}Domain Query Handling Method:
func (qs *queryHandler) GroupQueryModelByName(ctx context.Context, qry comby.Query, domainQry *GroupQueryModelByName) (*GroupQueryItemResponse, error)GroupQueryModel
Domain Query Struct:
type GroupQueryModel struct {
GroupUuid string `json:"groupUuid"`
IncludeHistory bool `json:"includeHistory,omitempty"`
}Domain Query Handling Method:
func (qs *queryHandler) GroupQueryModel(ctx context.Context, qry comby.Query, domainQry *GroupQueryModel) (*GroupQueryItemResponse, error)GroupQueryListResponse
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
type GroupQueryItemResponse struct {
Item *readmodel.GroupModel `json:"item,omitempty"`
}Events
GroupAddedEvent
Domain Event Struct:
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:
func (agg *Group) GroupAddedEvent(ctx context.Context, evt comby.Event, domainEvt *GroupAddedEvent) (error)GroupRemovedEvent
Domain Event Struct:
type GroupRemovedEvent struct {
Reason string `json:"reason,omitempty"`
}Domain Event Handling Method:
func (agg *Group) GroupRemovedEvent(ctx context.Context, evt comby.Event, domainEvt *GroupRemovedEvent) (error)GroupAttributeRemovedEvent
Domain Event Struct:
type GroupAttributeRemovedEvent struct {
Key string `json:"key"`
}Domain Event Handling Method:
func (agg *Group) GroupAttributeRemovedEvent(ctx context.Context, evt comby.Event, domainEvt *GroupAttributeRemovedEvent) (error)GroupAttributeSetEvent
Domain Event Struct:
type GroupAttributeSetEvent struct {
Key string `json:"key"`
Value any `json:"value"`
}Domain Event Handling Method:
func (agg *Group) GroupAttributeSetEvent(ctx context.Context, evt comby.Event, domainEvt *GroupAttributeSetEvent) (error)GroupUpdatedEvent
Domain Event Struct:
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:
func (agg *Group) GroupUpdatedEvent(ctx context.Context, evt comby.Event, domainEvt *GroupUpdatedEvent) (error)Aggregate
Aggregate Struct:
type Group struct {
*comby.BaseAggregate
// Value Objects
Permissions []string
Name string
Description string
}Methods
Add
func (agg *Group) Add(opts ) (error)Remove
func (agg *Group) Remove(opts ) (error)RemoveAttribute
func (agg *Group) RemoveAttribute(opts ) (error)SetAttribute
func (agg *Group) SetAttribute(opts ) (error)Update
func (agg *Group) Update(opts ) (error)Event Handlers
GroupReadmodel
| Domain Event | Method |
|---|---|
tenantAggregate.TenantCreatedEvent | TenantCreatedEvent |
tenantAggregate.TenantRemovedEvent | TenantRemovedEvent |
tenantAggregate.TenantUpdatedEvent | TenantUpdatedEvent |
groupAggregate.GroupAddedEvent | GroupAddedEvent |
groupAggregate.GroupUpdatedEvent | GroupUpdatedEvent |
groupAggregate.GroupRemovedEvent | GroupRemovedEvent |
groupAggregate.GroupAttributeSetEvent | GroupAttributeSetEvent |
groupAggregate.GroupAttributeRemovedEvent | GroupAttributeRemovedEvent |
Custom Permissions
| Name | Type | Comment |
|---|---|---|
| GroupCommandCreate | Command | Create new group |
| GroupCommandUpdate | Command | Update existing group |
| GroupCommandRemove | Command | Remove existing group |
| GroupCommandSetAttribute | Command | Set single attribute of existing group |
| GroupCommandRemoveAttribute | Command | Remove single attribute from existing group |
| GroupQueryList | Query | List groups |
| GroupQueryModel | Query | Get group by id |
| GroupQueryModelByName | Query | Get group by name |