Tenant
Overview
The Tenant domain is a default domain in comby, designed to represent an organization that can manage identities. These identities may be linked to existing accounts, creating a structured relationship between organizations and individual users. The domain is implemented through the Tenant aggregate, which models the organization, manages associated metadata, and reacts to domain events.
Concept
What is a Tenant?
A Tenant represents an organization, company, or isolated environment in a multi-tenant system. Each tenant has its own identities, groups, permissions, workspaces, and data. Tenants provide complete isolation between different organizations using the same Comby instance.
Key Characteristics:
- Complete Data Isolation: Each tenant's data is isolated from other tenants
- Unique Namespace: Each tenant has a unique name and UUID
- Resource Container: Contains identities, groups, workspaces, and aggregates
- Secret Management: Stores sensitive configuration (API keys, tokens, etc.)
- System Tenant: Special tenant for system administration
Multi-Tenant System
├── System Tenant (SYSTEM)
│ └── System Administrators
├── Tenant A (Acme Corp)
│ ├── Identities
│ ├── Groups & Permissions
│ ├── Workspaces
│ └── Business Data (Customers, Invoices, etc.)
└── Tenant B (Tech Inc)
├── Identities
├── Groups & Permissions
├── Workspaces
└── Business Data (Customers, Invoices, etc.)Architecture
Hierarchy
System
├── System Tenant (Special)
│ └── System Administrators
└── Tenants (Organizations)
├── Name (Unique identifier)
├── Attributes (Metadata)
├── Secrets (Sensitive configuration)
├── Identities (Users in this tenant)
├── Groups (Permissions in this tenant)
├── Workspaces (Projects/Teams)
└── Aggregates (All domain data)Related Entities
Tenant is connected to:
- Identity: Each identity belongs to exactly one tenant
- Group: Groups are tenant-specific
- Workspace: Workspaces belong to tenants
- All Aggregates: Every aggregate belongs to a tenant
- Auth: Auth domain uses tenant context for authorization
Domain Model
The Tenant serves as the central hub for all aggregates. While each aggregate operates independently, all comby default aggregates reference the Tenant by default to support a multi-tenancy system.
System Tenant
The System Tenant is a special tenant with elevated privileges:
Characteristics:
- UUID:
SYSTEM_TENANT_UUID(predefined constant) - Name:
SYSTEM_TENANT_NAME(reserved name) - Purpose: System administration and cross-tenant operations
- Privileges: Can view and manage all tenants
- Protected: Cannot be deleted or renamed
System Admin Group:
- Members of
SYSTEM_TENANT_GROUP_ADMIN_UUIDbypass all authorization - Can perform any operation across all tenants
- Should be assigned sparingly
Tenant Isolation
Data Isolation
Each tenant's data is completely isolated:
Tenant A:
- Identities: [Alice, Bob]
- Customers: [Customer 1, Customer 2]
- Invoices: [Invoice A, Invoice B]
Tenant B:
- Identities: [Charlie, Dave]
- Customers: [Customer 3, Customer 4]
- Invoices: [Invoice C, Invoice D]
Result: Alice cannot see Customer 3 or Invoice CQuery Context
Queries respect tenant context:
// System Tenant: Sees all tenants
TenantQueryList from SYSTEM_TENANT → [Tenant A, Tenant B, Tenant C]
// Regular Tenant: Sees only self
TenantQueryList from Tenant A → [Tenant A]Secrets Management
Tenants can store sensitive configuration as secrets:
Use Cases:
- API keys (Stripe, SendGrid, etc.)
- Database credentials
- OAuth client secrets
- Encryption keys
- Webhook secrets
Security:
- Secrets are stored separately from attributes
- Secrets are never exposed in logs or responses
- Access to secrets requires elevated permissions
Structure
The structure of a Tenant is simple and consists of the following elements:
- Name: The name of the tenant or organization.
- Secrets: A map for storing sensitive information, such as API keys or other credentials linked to the tenant.
A Tenant does not reference other aggregates, as it serves as the central entity. All other aggregates refer to the Tenant instead. In comby, multiple Tenants can be created and managed. The framework ensures that Tenants are isolated, and no data is shared between them.
Use Cases
1. SaaS Platform
Multi-Tenant SaaS:
├── System Tenant
│ └── Platform Administrators
├── Acme Corp (Enterprise Customer)
│ ├── 50 users
│ ├── Custom integrations
│ └── Dedicated support
├── Tech Startup (Small Customer)
│ ├── 5 users
│ ├── Standard features
│ └── Email support
└── ...2. White-Label Platform
White-Label Application:
├── Reseller A
│ ├── Branding: Logo, Colors
│ ├── Custom Domain
│ └── Sub-customers
└── Reseller B
├── Branding: Logo, Colors
├── Custom Domain
└── Sub-customers3. Enterprise Departments
Large Enterprise:
├── Engineering Department
│ ├── Dev Team
│ ├── QA Team
│ └── DevOps Team
├── Marketing Department
│ ├── Content Team
│ └── Design Team
└── Sales Department
├── Sales Reps
└── Account Managers4. Agency Management
Agency Platform:
├── Agency Tenant
│ └── Agency Employees
├── Client A Tenant
│ └── Client A Users
├── Client B Tenant
│ └── Client B Users
└── ...Features
- Complete data isolation
- Unique tenant names
- Secret management for sensitive data
- Custom attributes for metadata
- System tenant for administration
- Context-dependent queries
- Protected system tenant
- Tenant deletion cascade
Best Practices
Tenant Creation
- Use descriptive, organization-based names
- Create system tenant first during setup
- Assign initial admin identity immediately
- Set up default groups during creation
Secret Management
- Store only sensitive data as secrets
- Rotate secrets regularly
- Never log secret values
- Use descriptive secret keys
Attributes vs Secrets
Attributes (Public metadata):
- industry, size, plan, active, region
Secrets (Sensitive data):
- API keys, passwords, tokens, certificatesTenant Deletion
- Warning: Deleting a tenant is irreversible!
Before deletion:
- Export all data
- Notify all users
- Cancel subscriptions/services
- Remove integrations
- Archive for compliance
System Tenant
- Create during initial setup
- Limit system admin group membership
- Never delete system tenant
- Audit system admin actions
Troubleshooting
"Tenant with provided name already exists"
Cause: Tenant names must be unique across the system.
Solution: Choose a different tenant name or append a qualifier:
"Acme Corporation" → "Acme Corporation - East Division"Cannot create tenant with system tenant name
Cause: Trying to create a tenant with reserved name SYSTEM_TENANT_NAME.
Solution: System tenant name is reserved. Use a different name.
Tenant not visible in list
Check:
- Are you querying from the correct context?
- Regular tenants can only see themselves
- Only system tenant can see all tenants
Debug:
// Check request context
reqCtx := qry.GetReqCtx()
fmt.Printf("Sender Tenant: %s\n", reqCtx.SenderTenantUuid)
// System tenant sees all, others see only selfSecret not accessible
Cause: Secrets require elevated permissions and are not returned in regular queries.
Solution:
- Verify identity has permission to access secrets
- Use dedicated secret retrieval endpoint
- Never expose secrets in responses
Cross-tenant data access
Cause: Attempting to access data from another tenant.
Solution:
- Ensure all operations are within tenant context
- Cross-tenant operations require system admin privileges
- Check authorization middleware
Integration with Other Domains
Identity Domain
// Create tenant, then create first identity
tenantCmd := &tenant.TenantCommandCreate{...}
facade.DispatchCommand(ctx, tenantCmd)
// Create admin identity for tenant
identityCmd := &identity.IdentityCommandCreate{
IdentityUuid: uuid.New().String(),
AccountUuid: ownerAccountUuid,
GroupUuids: []string{adminGroupUuid},
}
identityCmd.SetTenantUuid(newTenantUuid)
facade.DispatchCommand(ctx, identityCmd)Group Domain
// Create default groups after tenant creation
createGroup("Administrators", []string{"Customer.Get"}, newTenantUuid)
createGroup("Members", []string{"Invoice.List", "Invoice.Get"}, newTenantUuid)
createGroup("Viewers", []string{"Invoice.Get"}, newTenantUuid)Auth Domain
- Auth readmodel tracks tenant context
- Authorization checks tenant membership
- Cross-tenant requests are blocked (except system admin)
All Domain Aggregates
Every aggregate belongs to a tenant:
cmd.SetTenantUuid(tenantUuid) // All commands must have tenant contextTenant Lifecycle
1. Provisioning
// Create tenant
CreateTenant("Acme Corp")
// → Create default groups
// → Create admin identity
// → Set up initial configuration
// → Send welcome email2. Active Use
// Normal operations:
- Users log in and select tenant
- Create/manage data within tenant
- Invite new users
- Configure integrations3. Maintenance
// Regular maintenance:
- Update tenant attributes
- Rotate secrets
- Review user access
- Monitor usage4. Offboarding
// Before deletion:
- Export all data
- Notify users
- Cancel services
- Archive records
- Delete tenantSecurity Considerations
Data Isolation
- Tenants cannot access other tenants' data
- All queries are tenant-scoped
- Authorization checks tenant context
- Event store enforces tenant separation
Secret Security
- Secrets are encrypted at rest
- Never log secret values
- Secrets are not returned in responses
- Access requires elevated permissions
- Implement secret rotation policies
System Tenant Access
- System tenant has god-mode access
- Limit system admin assignments
- Audit all system tenant operations
- Use system tenant only for administration
Tenant Deletion
- Deletion cascades to all tenant data
- Implement soft delete for compliance
- Require confirmation before deletion
- Archive data before deletion
Commands
- TenantCommandCreate
- TenantCommandRemove
- TenantCommandRemoveAttribute
- TenantCommandRemoveSecret
- TenantCommandSetAttribute
- TenantCommandSetSecret
- TenantCommandUpdate
TenantCommandCreate
Domain Command Struct:
type TenantCommandCreate struct {
TenantUuid string `json:"tenantUuid"`
Name string `json:"name"`
Attributes string `json:"attributes,omitempty"`
}Domain Command Handling Method:
func (cs *commandHandler) TenantCommandCreate(ctx context.Context, cmd comby.Command, domainCmd *TenantCommandCreate) ([]comby.Event, error)TenantCommandRemove
Domain Command Struct:
type TenantCommandRemove struct {
TenantUuid string `json:"tenantUuid"`
}Domain Command Handling Method:
func (cs *commandHandler) TenantCommandRemove(ctx context.Context, cmd comby.Command, domainCmd *TenantCommandRemove) ([]comby.Event, error)TenantCommandRemoveAttribute
Domain Command Struct:
type TenantCommandRemoveAttribute struct {
TenantUuid string `json:"tenantUuid"`
Key string `json:"key"`
}Domain Command Handling Method:
func (ch *commandHandler) TenantCommandRemoveAttribute(ctx context.Context, cmd comby.Command, domainCmd *TenantCommandRemoveAttribute) ([]comby.Event, error)TenantCommandRemoveSecret
Domain Command Struct:
type TenantCommandRemoveSecret struct {
TenantUuid string `json:"tenantUuid"`
SecretKey string `json:"secretKey"`
}Domain Command Handling Method:
func (ch *commandHandler) TenantCommandRemoveSecret(ctx context.Context, cmd comby.Command, domainCmd *TenantCommandRemoveSecret) ([]comby.Event, error)TenantCommandSetAttribute
Domain Command Struct:
type TenantCommandSetAttribute struct {
TenantUuid string `json:"tenantUuid"`
Key string `json:"key"`
Value any `json:"value"`
}Domain Command Handling Method:
func (cs *commandHandler) TenantCommandSetAttribute(ctx context.Context, cmd comby.Command, domainCmd *TenantCommandSetAttribute) ([]comby.Event, error)TenantCommandSetSecret
Domain Command Struct:
type TenantCommandSetSecret struct {
TenantUuid string `json:"tenantUuid"`
SecretKey string `json:"secretKey"`
SecretValue string `json:"secretValue,omitempty"`
}Domain Command Handling Method:
func (cs *commandHandler) TenantCommandSetSecret(ctx context.Context, cmd comby.Command, domainCmd *TenantCommandSetSecret) ([]comby.Event, error)TenantCommandUpdate
Domain Command Struct:
type TenantCommandUpdate struct {
TenantUuid string `json:"tenantUuid"`
Name string `json:"name,omitempty"`
Attributes string `json:"attributes,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) TenantCommandUpdate(ctx context.Context, cmd comby.Command, domainCmd *TenantCommandUpdate) ([]comby.Event, error)Queries
Domain Query Structs:
Domain Query Responses:
TenantQueryList
TenantQueryList returns a list of tenants based on the context of the requestor.
This query determines the list of tenants a requestor is allowed to access, with behavior depending on the requestor's context. The query outcome is classified into two distinct cases:
- System Tenant Context:
- When the requestor represents the system tenant, the query retrieves a complete list of all tenants.
- This includes tenants from all contexts, providing global visibility for the system tenant.
- Specific Tenant Context:
- If the requestor represents a specific tenant, the query returns a list containing only the tenant associated with the requestor.
- This ensures that the tenant has access only to its own information, adhering to strict isolation principles in a multi-tenant architecture.
Domain Query Struct:
type TenantQueryList struct {
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) TenantQueryList(ctx context.Context, qry comby.Query, domainQry *TenantQueryList) (*TenantQueryListResponse, error)TenantQueryModelByName
Domain Query Struct:
type TenantQueryModelByName struct {
Name string `json:"name"`
}Domain Query Handling Method:
func (qs *queryHandler) TenantQueryModelByName(ctx context.Context, qry comby.Query, domainQry *TenantQueryModelByName) (*TenantQueryItemResponse, error)TenantQueryModel
Domain Query Struct:
type TenantQueryModel struct {
TenantUuid string `json:"tenantUuid"`
IncludeHistory bool `json:"includeHistory,omitempty"`
}Domain Query Handling Method:
func (qs *queryHandler) TenantQueryModel(ctx context.Context, qry comby.Query, domainQry *TenantQueryModel) (*TenantQueryItemResponse, error)TenantQueryListResponse
type TenantQueryListResponse struct {
Items []*readmodel.TenantModel `json:"items,omitempty"`
Total int64 `json:"total,omitempty"`
Page int64 `json:"page,omitempty"`
PageSize int64 `json:"pageSize,omitempty"`
}TenantQueryItemResponse
type TenantQueryItemResponse struct {
Item *readmodel.TenantModel `json:"item,omitempty"`
}Events
- TenantCreatedEvent
- TenantRemovedEvent
- TenantAttributeRemovedEvent
- TenantSecretRemovedEvent
- TenantAttributeSetEvent
- TenantSecretSetEvent
- TenantUpdatedEvent
TenantCreatedEvent
Domain Event Struct:
type TenantCreatedEvent struct {
Name string `json:"name"`
Attributes string `json:"attributes,omitempty"`
}Domain Event Handling Method:
func (agg *Tenant) TenantCreatedEvent(ctx context.Context, evt comby.Event, domainEvt *TenantCreatedEvent) (error)TenantRemovedEvent
Domain Event Struct:
type TenantRemovedEvent struct {
Reason string `json:"reason,omitempty"`
}Domain Event Handling Method:
func (agg *Tenant) TenantRemovedEvent(ctx context.Context, evt comby.Event, domainEvt *TenantRemovedEvent) (error)TenantAttributeRemovedEvent
Domain Event Struct:
type TenantAttributeRemovedEvent struct {
Key string `json:"key"`
}Domain Event Handling Method:
func (agg *Tenant) TenantAttributeRemovedEvent(ctx context.Context, evt comby.Event, domainEvt *TenantAttributeRemovedEvent) (error)TenantSecretRemovedEvent
Domain Event Struct:
type TenantSecretRemovedEvent struct {
SecretKey string `json:"secretKey"`
}Domain Event Handling Method:
func (agg *Tenant) TenantSecretRemovedEvent(ctx context.Context, evt comby.Event, domainEvt *TenantSecretRemovedEvent) (error)TenantAttributeSetEvent
Domain Event Struct:
type TenantAttributeSetEvent struct {
Key string `json:"key"`
Value any `json:"value"`
}Domain Event Handling Method:
func (agg *Tenant) TenantAttributeSetEvent(ctx context.Context, evt comby.Event, domainEvt *TenantAttributeSetEvent) (error)TenantSecretSetEvent
Domain Event Struct:
type TenantSecretSetEvent struct {
SecretKey string `json:"SecretKey"`
SecretValue string `json:"SecretValue,omitempty"`
}Domain Event Handling Method:
func (agg *Tenant) TenantSecretSetEvent(ctx context.Context, evt comby.Event, domainEvt *TenantSecretSetEvent) (error)TenantUpdatedEvent
Domain Event Struct:
type TenantUpdatedEvent struct {
Name string `json:"name,omitempty"`
Attributes string `json:"attributes,omitempty"`
}Domain Event Handling Method:
func (agg *Tenant) TenantUpdatedEvent(ctx context.Context, evt comby.Event, domainEvt *TenantUpdatedEvent) (error)Aggregate
Aggregate Struct:
type Tenant struct {
*comby.BaseAggregate
// Value Objects
Name string
Secrets *comby.Attributes
}Methods
Create
func (agg *Tenant) Create(opts ) (error)Remove
func (agg *Tenant) Remove(opts ) (error)RemoveAttribute
func (agg *Tenant) RemoveAttribute(opts ) (error)RemoveSecret
func (agg *Tenant) RemoveSecret(opts ) (error)SetAttribute
func (agg *Tenant) SetAttribute(opts ) (error)SetSecret
func (agg *Tenant) SetSecret(opts ) (error)Update
func (agg *Tenant) Update(opts ) (error)Event Handlers
TenantReadmodel
| Domain Event | Method |
|---|---|
webhookAggregate.WebhookRemovedEvent | WebhookRemovedEvent |
webhookAggregate.WebhookAddedEvent | WebhookAddedEvent |
tenantAggregate.TenantCreatedEvent | TenantCreatedEvent |
tenantAggregate.TenantSecretRemovedEvent | TenantSecretRemovedEvent |
tenantAggregate.TenantSecretSetEvent | TenantSecretSetEvent |
tenantAggregate.TenantAttributeRemovedEvent | TenantAttributeRemovedEvent |
tenantAggregate.TenantAttributeSetEvent | TenantAttributeSetEvent |
tenantAggregate.TenantUpdatedEvent | TenantUpdatedEvent |
tenantAggregate.TenantRemovedEvent | TenantRemovedEvent |
invitationAggregate.InvitationCreatedEvent | InvitationCreatedEvent |
invitationAggregate.InvitationRemovedEvent | InvitationRemovedEvent |
identityAggregate.IdentityProfileUpdatedEvent | IdentityProfileUpdatedEvent |
identityAggregate.IdentityRemovedEvent | IdentityRemovedEvent |
identityAggregate.IdentityCreatedEvent | IdentityCreatedEvent |
groupAggregate.GroupUpdatedEvent | GroupUpdatedEvent |
groupAggregate.GroupRemovedEvent | GroupRemovedEvent |
groupAggregate.GroupAddedEvent | GroupAddedEvent |
assetAggregate.AssetAddedEvent | AssetAddedEvent |
assetAggregate.AssetRemovedEvent | AssetRemovedEvent |