Skip to content

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)

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_UUID bypass 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 C

Query Context

Queries respect tenant context:

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

3. Enterprise Departments

Large Enterprise:
├── Engineering Department
│   ├── Dev Team
│   ├── QA Team
│   └── DevOps Team
├── Marketing Department
│   ├── Content Team
│   └── Design Team
└── Sales Department
    ├── Sales Reps
    └── Account Managers

4. 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, certificates

Tenant Deletion

  • Warning: Deleting a tenant is irreversible!

Before deletion:

  1. Export all data
  2. Notify all users
  3. Cancel subscriptions/services
  4. Remove integrations
  5. 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:

  1. Are you querying from the correct context?
  2. Regular tenants can only see themselves
  3. Only system tenant can see all tenants

Debug:

go
// Check request context
reqCtx := qry.GetReqCtx()
fmt.Printf("Sender Tenant: %s\n", reqCtx.SenderTenantUuid)

// System tenant sees all, others see only self

Secret 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

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

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

go
cmd.SetTenantUuid(tenantUuid)  // All commands must have tenant context

Tenant Lifecycle

1. Provisioning

go
// Create tenant
CreateTenant("Acme Corp")
// → Create default groups
// → Create admin identity
// → Set up initial configuration
// → Send welcome email

2. Active Use

go
// Normal operations:
- Users log in and select tenant
- Create/manage data within tenant
- Invite new users
- Configure integrations

3. Maintenance

go
// Regular maintenance:
- Update tenant attributes
- Rotate secrets
- Review user access
- Monitor usage

4. Offboarding

go
// Before deletion:
- Export all data
- Notify users
- Cancel services
- Archive records
- Delete tenant

Security 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

Domain Command Struct:

go
type TenantCommandCreate struct {
	TenantUuid string `json:"tenantUuid"`
	Name       string `json:"name"`
	Attributes string `json:"attributes,omitempty"`
}

Domain Command Handling Method:

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

TenantCommandRemove

Domain Command Struct:

go
type TenantCommandRemove struct {
	TenantUuid string `json:"tenantUuid"`
}

Domain Command Handling Method:

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

TenantCommandRemoveAttribute

Domain Command Struct:

go
type TenantCommandRemoveAttribute struct {
	TenantUuid string `json:"tenantUuid"`
	Key        string `json:"key"`
}

Domain Command Handling Method:

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

TenantCommandRemoveSecret

Domain Command Struct:

go
type TenantCommandRemoveSecret struct {
	TenantUuid string `json:"tenantUuid"`
	SecretKey  string `json:"secretKey"`
}

Domain Command Handling Method:

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

TenantCommandSetAttribute

Domain Command Struct:

go
type TenantCommandSetAttribute struct {
	TenantUuid string `json:"tenantUuid"`
	Key        string `json:"key"`
	Value      any    `json:"value"`
}

Domain Command Handling Method:

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

TenantCommandSetSecret

Domain Command Struct:

go
type TenantCommandSetSecret struct {
	TenantUuid  string `json:"tenantUuid"`
	SecretKey   string `json:"secretKey"`
	SecretValue string `json:"secretValue,omitempty"`
}

Domain Command Handling Method:

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

TenantCommandUpdate

Domain Command Struct:

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

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

  1. 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.
  1. 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:

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

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

TenantQueryModelByName

Domain Query Struct:

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

Domain Query Handling Method:

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

TenantQueryModel

Domain Query Struct:

go
type TenantQueryModel struct {
	TenantUuid     string `json:"tenantUuid"`
	IncludeHistory bool   `json:"includeHistory,omitempty"`
}

Domain Query Handling Method:

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

TenantQueryListResponse

go
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

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

Events

TenantCreatedEvent

Domain Event Struct:

go
type TenantCreatedEvent struct {
	Name       string `json:"name"`
	Attributes string `json:"attributes,omitempty"`
}

Domain Event Handling Method:

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

TenantRemovedEvent

Domain Event Struct:

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

Domain Event Handling Method:

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

TenantAttributeRemovedEvent

Domain Event Struct:

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

Domain Event Handling Method:

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

TenantSecretRemovedEvent

Domain Event Struct:

go
type TenantSecretRemovedEvent struct {
	SecretKey string `json:"secretKey"`
}

Domain Event Handling Method:

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

TenantAttributeSetEvent

Domain Event Struct:

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

Domain Event Handling Method:

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

TenantSecretSetEvent

Domain Event Struct:

go
type TenantSecretSetEvent struct {
	SecretKey   string `json:"SecretKey"`
	SecretValue string `json:"SecretValue,omitempty"`
}

Domain Event Handling Method:

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

TenantUpdatedEvent

Domain Event Struct:

go
type TenantUpdatedEvent struct {
	Name       string `json:"name,omitempty"`
	Attributes string `json:"attributes,omitempty"`
}

Domain Event Handling Method:

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

Aggregate

Aggregate Struct:

go
type Tenant struct {
	*comby.BaseAggregate
	// Value Objects
	Name    string
	Secrets *comby.Attributes
}

Methods

Create

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

Remove

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

RemoveAttribute

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

RemoveSecret

go
func (agg *Tenant) RemoveSecret(opts ) (error)

SetAttribute

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

SetSecret

go
func (agg *Tenant) SetSecret(opts ) (error)

Update

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

Event Handlers

TenantReadmodel

Domain EventMethod
webhookAggregate.WebhookRemovedEventWebhookRemovedEvent
webhookAggregate.WebhookAddedEventWebhookAddedEvent
tenantAggregate.TenantCreatedEventTenantCreatedEvent
tenantAggregate.TenantSecretRemovedEventTenantSecretRemovedEvent
tenantAggregate.TenantSecretSetEventTenantSecretSetEvent
tenantAggregate.TenantAttributeRemovedEventTenantAttributeRemovedEvent
tenantAggregate.TenantAttributeSetEventTenantAttributeSetEvent
tenantAggregate.TenantUpdatedEventTenantUpdatedEvent
tenantAggregate.TenantRemovedEventTenantRemovedEvent
invitationAggregate.InvitationCreatedEventInvitationCreatedEvent
invitationAggregate.InvitationRemovedEventInvitationRemovedEvent
identityAggregate.IdentityProfileUpdatedEventIdentityProfileUpdatedEvent
identityAggregate.IdentityRemovedEventIdentityRemovedEvent
identityAggregate.IdentityCreatedEventIdentityCreatedEvent
groupAggregate.GroupUpdatedEventGroupUpdatedEvent
groupAggregate.GroupRemovedEventGroupRemovedEvent
groupAggregate.GroupAddedEventGroupAddedEvent
assetAggregate.AssetAddedEventAssetAddedEvent
assetAggregate.AssetRemovedEventAssetRemovedEvent