Skip to content

Asset

Overview

The Asset domain in comby is designed to manage digital assets, such as files, documents, or media, along with their associated metadata and storage information. It provides a structured framework for tracking asset lifecycle events, including creation, updates, and removal. The Asset aggregate is at the core of this domain, encapsulating the properties and behaviors of assets while maintaining consistency through event sourcing.

Concept

What is an Asset?

An Asset represents a file or binary object stored in the system. Assets can be images, documents, videos, or any other file type. Each asset is associated with a tenant and optionally with a workspace for fine-grained access control.

Key Characteristics:

  • Bucket-Based Storage: Assets are stored in buckets (tenant-specific or public)
  • Workspace Support: Assets can be scoped to specific workspaces
  • Visibility Control: Public or private asset visibility
  • Metadata Management: Name, size, content type, and custom attributes
  • Ephemeral Upload Flow: Secure two-stage upload process
Asset
├── Metadata (Name, Size, ContentType, Path)
├── Storage Location (BucketName, ObjectName)
├── Ownership (IdentityUuid, TenantUuid)
└── Workspace Scope (Optional WorkspaceUuid)

Architecture

Hierarchy

System
└── Tenant
    ├── Assets (Tenant-level)
    │   ├── Private Bucket (tenant-uuid)
    │   └── Public Bucket (tenant-uuid-public)
    └── Workspaces
        └── Assets (Workspace-level)
            ├── Private Bucket (tenant-uuid)
            └── Public Bucket (tenant-uuid-public)

Asset is connected to:

  • Tenant: Each asset belongs to a tenant
  • Workspace: Assets can be scoped to workspaces
  • Identity: Assets have an owner identity
  • DataStore: Backend storage system (S3, MinIO, etc.)

Domain Model

go
type Asset struct {
    AggregateUuid string

    // References
    IdentityUuid  string
    WorkspaceUuid string  // Optional workspace scope

    // Asset Metadata
    Name        string
    Size        int64
    ContentType string
    Path        string

    // Storage Location
    BucketName string
    ObjectName string
}

Asset Visibility

Private Assets

  • Stored in tenant-specific bucket: {tenantUuid}
  • Require authentication and authorization
  • Only accessible by authorized users within the tenant/workspace

Public Assets

  • Stored in public bucket: {tenantUuid}-public
  • Accessible without authentication via direct URL
  • Set via attribute: default-visibility:public

Upload Flow

Assets follow a secure two-stage upload process:

  1. Ephemeral Upload: File is uploaded to temporary ephemeral bucket
  2. Authorization Check: Command validates permissions
  3. Persistent Copy: File is copied to tenant/workspace bucket
  4. Cleanup: Ephemeral file is deleted
  5. Asset Created: Asset aggregate is created with metadata

This ensures files are only persisted after successful authorization.

Structure

The Asset aggregate extends the BaseAggregate, leveraging comby's event-sourcing capabilities for managing state and tracking changes. It defines several fields to represent asset-specific and storage-related attributes:

  • References:
    • IdentityUuid: Links the asset to the identity that owns or created it.
  • Entities: Reserved for nested data structures (not included in the default implementation).
  • Value Objects:
    • Asset Metadata:
      • Name: The name of the asset.
      • Size: The size of the asset in bytes.
      • ContentType: The MIME type of the asset (e.g., image/png, application/pdf).
      • Path: The logical or relative path of the asset.
    • Storage Metadata:
      • BucketName: The storage bucket where the asset is stored.
      • ObjectName: The unique identifier for the asset in the storage system.
    • Tags: A list of tags for categorizing or labeling the asset.
    • Public: A boolean indicating whether the asset is publicly accessible.
    • Additionals: A map for storing custom metadata or additional attributes.

This structure provides flexibility for managing diverse types of assets and their storage configurations.

Use Cases

1. Profile Picture Management

1. User uploads profile picture
2. Asset created with default-visibility:public
3. Stored in public bucket
4. Accessible via direct URL: /assets/{bucket}/{object}

2. Document Storage in Workspace

1. User uploads document to workspace
2. Asset scoped to workspace
3. Only workspace members can access
4. Download via authenticated endpoint

3. File Sharing

1. User uploads file
2. Asset created as private
3. User updates attribute to public
4. Share direct public URL

4. Multi-Tenant File Organization

Tenant A
├── Private Assets (contracts, internal docs)
└── Public Assets (logos, marketing materials)

Tenant B
├── Private Assets (customer data, reports)
└── Public Assets (product images)

Storage Backends

The Asset domain integrates with object storage backends through the DataStore interface:

Supported Backends

  • MinIO: S3-compatible object storage
  • AWS S3: Amazon Simple Storage Service
  • Google Cloud Storage: GCS
  • Azure Blob Storage: Azure

Configuration

Configure storage backend in your application:

go
dataStore := // Initialize your storage backend
facade.SetDataStore(dataStore)

Features

  • Tenant-level and workspace-level asset organization
  • Public and private asset visibility
  • Secure two-stage upload process
  • Multiple storage backend support
  • File metadata management (name, size, type)
  • Custom attributes support
  • Direct download URLs for public assets
  • Authorization integration
  • Workspace isolation

Best Practices

Asset Organization

  • Use workspaces for project-specific files
  • Use tenant-level for shared resources
  • Set appropriate visibility (public/private)
  • Use descriptive asset names

Storage Management

  • Implement file size limits
  • Validate file types before upload
  • Clean up orphaned files periodically
  • Monitor storage usage per tenant

Security

  • Validate file content types
  • Scan uploads for malware
  • Use private visibility by default
  • Implement rate limiting for uploads

Performance

  • Use CDN for public assets
  • Implement caching headers
  • Compress large files before upload
  • Use streaming for large downloads

Troubleshooting

Upload fails with "asset already exists"

Cause: Asset UUID already exists in the system.

Solution: Generate a new unique UUID for the asset.

Download returns 404

Check:

  1. Does the asset exist?
  2. Is the user authorized to access the asset?
  3. Is the asset in the correct bucket?
  4. For workspace assets, is the user a workspace member?

Public asset not accessible

Check:

  1. Is the default-visibility attribute set to public?
  2. Is the asset in the public bucket ({tenantUuid}-public)?
  3. Is the storage backend configured to allow public access?

File size limit exceeded

Solution: Configure the web server to allow larger request bodies:

go
huma.Operation{
    MaxBodyBytes: -1,  // No limit (let web server handle)
}

Or set a specific limit:

go
huma.Operation{
    MaxBodyBytes: 10 * 1024 * 1024,  // 10 MB
}

Content Type Detection

Assets store the content type for proper file handling:

Common Content Types

  • Images: image/png, image/jpeg, image/gif
  • Documents: application/pdf, application/msword
  • Spreadsheets: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
  • Archives: application/zip, application/x-tar

Setting Content Type

Content type is automatically detected during upload or can be explicitly set:

go
AssetContentType: "application/pdf"

Attributes

Assets support custom attributes for metadata and categorization:

Common Attribute Uses

  • Visibility: default-visibility:public or default-visibility:private
  • Categories: category:financial, category:marketing
  • Tags: tags:important,urgent
  • Version: version:1.0, version:2.0
  • Status: status:draft, status:published

Format

Attributes are stored as comma-separated key:value pairs:

key1:value1,key2:value2,key3:value3

Commands

AssetCommandAdd

Domain Command Struct:

go
type AssetCommandAdd struct {
	AssetUuid        string `json:"assetUuid"`
	IdentityUuid     string `json:"identityUuid,omitempty"`
	AssetName        string `json:"assetName,omitempty"`
	AssetSize        int64  `json:"assetSize,omitempty"`
	AssetContentType string `json:"assetContentType,omitempty"`
	WorkspaceUuid    string `json:"workspaceUuid,omitempty"` // Optional workspace UUID for workspace-scoped assets
	Attributes       string `json:"attributes,omitempty"`
}

Domain Command Handling Method:

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

AssetCommandRemove

Domain Command Struct:

go
type AssetCommandRemove struct {
	AssetUuid string `json:"assetUuid"`
}

Domain Command Handling Method:

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

AssetCommandRemoveAttribute

Domain Command Struct:

go
type AssetCommandRemoveAttribute struct {
	AssetUuid string `json:"assetUuid"`
	Key       string `json:"key"`
}

Domain Command Handling Method:

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

AssetCommandSetAttribute

Domain Command Struct:

go
type AssetCommandSetAttribute struct {
	AssetUuid string `json:"assetUuid"`
	Key       string `json:"key"`
	Value     any    `json:"value"`
}

Domain Command Handling Method:

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

AssetCommandUpdate

Domain Command Struct:

go
type AssetCommandUpdate struct {
	IdentityUuid     string   `json:"identityUuid"`
	AssetUuid        string   `json:"assetUuid"`
	AssetName        string   `json:"assetName,omitempty"`
	AssetSize        int64    `json:"assetSize,omitempty"`
	AssetContentType string   `json:"assetContentType,omitempty"`
	Attributes       string   `json:"attributes,omitempty"`
	PatchedFields    []string `json:"patchedFields"`
}

Domain Command Handling Method:

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

Queries

Domain Query Structs:

Domain Query Responses:

AssetQueryDownload

Domain Query Struct:

go
type AssetQueryDownload struct {
	AssetUuid string `json:"assetUuid"`
}

Domain Query Handling Method:

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

AssetQueryList

Domain Query Struct:

go
type AssetQueryList struct {
	TenantUuid     string `json:"tenantUuid"`
	WorkspaceUuid  string `json:"workspaceUuid,omitempty"`
	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) AssetQueryList(ctx context.Context, qry comby.Query, domainQry *AssetQueryList) (*AssetQueryListResponse, error)

AssetQueryModelByBucketObject

Domain Query Struct:

go
type AssetQueryModelByBucketObject struct {
	BucketName string `json:"bucketName"`
	ObjectName string `json:"objectName"`
}

Domain Query Handling Method:

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

AssetQueryModel

Domain Query Struct:

go
type AssetQueryModel struct {
	AssetUuid      string `json:"assetUuid"`
	IncludeHistory bool   `json:"includeHistory,omitempty"`
}

Domain Query Handling Method:

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

AssetQueryItemResponse

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

AssetQueryListResponse

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

Events

AssetAddedEvent

Domain Event Struct:

go
type AssetAddedEvent struct {
	IdentityUuid     string `json:"identityUuid,omitempty"`
	AssetName        string `json:"assetName,omitempty"`
	AssetSize        int64  `json:"assetSize,omitempty"`
	AssetContentType string `json:"assetContentType,omitempty"`
	BucketName       string `json:"bucketName,omitempty"`
	ObjectName       string `json:"objectName,omitempty"`
	WorkspaceUuid    string `json:"workspaceUuid,omitempty"` // Optional workspace UUID for workspace-scoped assets
	Attributes       string `json:"attributes,omitempty"`
}

Domain Event Handling Method:

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

AssetRemovedEvent

Domain Event Struct:

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

Domain Event Handling Method:

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

AssetAttributeRemovedEvent

Domain Event Struct:

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

Domain Event Handling Method:

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

AssetAttributeSetEvent

Domain Event Struct:

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

Domain Event Handling Method:

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

AssetUpdatedEvent

Domain Event Struct:

go
type AssetUpdatedEvent struct {
	IdentityUuid     string `json:"identityUuid,omitempty"`
	AssetName        string `json:"assetName,omitempty"`
	AssetSize        int64  `json:"assetSize,omitempty"`
	AssetContentType string `json:"assetContentType,omitempty"`
	BucketName       string `json:"bucketName,omitempty"`
	ObjectName       string `json:"objectName,omitempty"`
	Attributes       string `json:"attributes,omitempty"`
}

Domain Event Handling Method:

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

Aggregate

Aggregate Struct:

go
type Asset struct {
	*comby.BaseAggregate
	// References
	IdentityUuid string
	// WorkspaceUuid - when set, this asset is scoped to a specific workspace
	WorkspaceUuid string
	// Value Objects (asset)
	Name        string
	Size        int64
	ContentType string
	Path        string
	// Value Objects (storage)
	BucketName string
	ObjectName string
}

Methods

Add

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

Remove

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

RemoveAttribute

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

SetAttribute

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

Update

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

Event Handlers

AssetReadmodel

Domain EventMethod
workspaceAggregate.WorkspaceUpdatedEventWorkspaceUpdatedEvent
workspaceAggregate.WorkspaceRemovedEventWorkspaceRemovedEvent
workspaceAggregate.WorkspaceCreatedEventWorkspaceCreatedEvent
tenantAggregate.TenantCreatedEventTenantCreatedEvent
tenantAggregate.TenantUpdatedEventTenantUpdatedEvent
tenantAggregate.TenantRemovedEventTenantRemovedEvent
identityAggregate.IdentityProfileUpdatedEventIdentityProfileUpdatedEvent
identityAggregate.IdentityRemovedEventIdentityRemovedEvent
identityAggregate.IdentityCreatedEventIdentityCreatedEvent
assetAggregate.AssetAddedEventAssetAddedEvent
assetAggregate.AssetAttributeRemovedEventAssetAttributeRemovedEvent
assetAggregate.AssetAttributeSetEventAssetAttributeSetEvent
assetAggregate.AssetRemovedEventAssetRemovedEvent
assetAggregate.AssetUpdatedEventAssetUpdatedEvent

Custom Permissions

NameTypeComment
AssetCommandAddCommandCreate and upload asset
AssetCommandRemoveCommandRemove existing asset
AssetCommandUpdateCommandUpdate existing asset
AssetCommandSetAttributeCommandSet single attribute for existing asset
AssetCommandRemoveAttributeCommandRemove single attribute from existing asset
AssetQueryDownloadQueryDownload existing asset
AssetQueryListQueryList all assets
AssetQueryModelQueryGet asset
AssetQueryModelByBucketObjectQueryGet asset by bucket and object name