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)Related Entities
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
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:
- Ephemeral Upload: File is uploaded to temporary ephemeral bucket
- Authorization Check: Command validates permissions
- Persistent Copy: File is copied to tenant/workspace bucket
- Cleanup: Ephemeral file is deleted
- 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.
- Asset Metadata:
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 endpoint3. File Sharing
1. User uploads file
2. Asset created as private
3. User updates attribute to public
4. Share direct public URL4. 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:
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:
- Does the asset exist?
- Is the user authorized to access the asset?
- Is the asset in the correct bucket?
- For workspace assets, is the user a workspace member?
Public asset not accessible
Check:
- Is the
default-visibilityattribute set topublic? - Is the asset in the public bucket (
{tenantUuid}-public)? - Is the storage backend configured to allow public access?
File size limit exceeded
Solution: Configure the web server to allow larger request bodies:
huma.Operation{
MaxBodyBytes: -1, // No limit (let web server handle)
}Or set a specific limit:
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:
AssetContentType: "application/pdf"Attributes
Assets support custom attributes for metadata and categorization:
Common Attribute Uses
- Visibility:
default-visibility:publicordefault-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:value3Commands
- AssetCommandAdd
- AssetCommandRemove
- AssetCommandRemoveAttribute
- AssetCommandSetAttribute
- AssetCommandUpdate
AssetCommandAdd
Domain Command Struct:
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:
func (cs *commandHandler) AssetCommandAdd(ctx context.Context, cmd comby.Command, domainCmd *AssetCommandAdd) ([]comby.Event, error)AssetCommandRemove
Domain Command Struct:
type AssetCommandRemove struct {
AssetUuid string `json:"assetUuid"`
}Domain Command Handling Method:
func (cs *commandHandler) AssetCommandRemove(ctx context.Context, cmd comby.Command, domainCmd *AssetCommandRemove) ([]comby.Event, error)AssetCommandRemoveAttribute
Domain Command Struct:
type AssetCommandRemoveAttribute struct {
AssetUuid string `json:"assetUuid"`
Key string `json:"key"`
}Domain Command Handling Method:
func (cs *commandHandler) AssetCommandRemoveAttribute(ctx context.Context, cmd comby.Command, domainCmd *AssetCommandRemoveAttribute) ([]comby.Event, error)AssetCommandSetAttribute
Domain Command Struct:
type AssetCommandSetAttribute struct {
AssetUuid string `json:"assetUuid"`
Key string `json:"key"`
Value any `json:"value"`
}Domain Command Handling Method:
func (cs *commandHandler) AssetCommandSetAttribute(ctx context.Context, cmd comby.Command, domainCmd *AssetCommandSetAttribute) ([]comby.Event, error)AssetCommandUpdate
Domain Command Struct:
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:
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:
type AssetQueryDownload struct {
AssetUuid string `json:"assetUuid"`
}Domain Query Handling Method:
func (qs *queryHandler) AssetQueryDownload(ctx context.Context, qry comby.Query, domainQry *AssetQueryDownload) (*AssetQueryItemResponse, error)AssetQueryList
Domain Query Struct:
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:
func (qs *queryHandler) AssetQueryList(ctx context.Context, qry comby.Query, domainQry *AssetQueryList) (*AssetQueryListResponse, error)AssetQueryModelByBucketObject
Domain Query Struct:
type AssetQueryModelByBucketObject struct {
BucketName string `json:"bucketName"`
ObjectName string `json:"objectName"`
}Domain Query Handling Method:
func (qs *queryHandler) AssetQueryModelByBucketObject(ctx context.Context, qry comby.Query, domainQry *AssetQueryModelByBucketObject) (*AssetQueryItemResponse, error)AssetQueryModel
Domain Query Struct:
type AssetQueryModel struct {
AssetUuid string `json:"assetUuid"`
IncludeHistory bool `json:"includeHistory,omitempty"`
}Domain Query Handling Method:
func (qs *queryHandler) AssetQueryModel(ctx context.Context, qry comby.Query, domainQry *AssetQueryModel) (*AssetQueryItemResponse, error)AssetQueryItemResponse
type AssetQueryItemResponse struct {
Item *readmodel.AssetModel `json:"item,omitempty"`
}AssetQueryListResponse
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:
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:
func (agg *Asset) AssetAddedEvent(ctx context.Context, evt comby.Event, domainEvt *AssetAddedEvent) (error)AssetRemovedEvent
Domain Event Struct:
type AssetRemovedEvent struct {
Reason string `json:"reason,omitempty"`
}Domain Event Handling Method:
func (agg *Asset) AssetRemovedEvent(ctx context.Context, evt comby.Event, domainEvt *AssetRemovedEvent) (error)AssetAttributeRemovedEvent
Domain Event Struct:
type AssetAttributeRemovedEvent struct {
Key string `json:"key"`
}Domain Event Handling Method:
func (agg *Asset) AssetAttributeRemovedEvent(ctx context.Context, evt comby.Event, domainEvt *AssetAttributeRemovedEvent) (error)AssetAttributeSetEvent
Domain Event Struct:
type AssetAttributeSetEvent struct {
Key string `json:"key"`
Value any `json:"value"`
}Domain Event Handling Method:
func (agg *Asset) AssetAttributeSetEvent(ctx context.Context, evt comby.Event, domainEvt *AssetAttributeSetEvent) (error)AssetUpdatedEvent
Domain Event Struct:
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:
func (agg *Asset) AssetUpdatedEvent(ctx context.Context, evt comby.Event, domainEvt *AssetUpdatedEvent) (error)Aggregate
Aggregate Struct:
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
func (agg *Asset) Add(opts ) (error)Remove
func (agg *Asset) Remove(opts ) (error)RemoveAttribute
func (agg *Asset) RemoveAttribute(opts ) (error)SetAttribute
func (agg *Asset) SetAttribute(opts ) (error)Update
func (agg *Asset) Update(opts ) (error)Event Handlers
AssetReadmodel
| Domain Event | Method |
|---|---|
workspaceAggregate.WorkspaceUpdatedEvent | WorkspaceUpdatedEvent |
workspaceAggregate.WorkspaceRemovedEvent | WorkspaceRemovedEvent |
workspaceAggregate.WorkspaceCreatedEvent | WorkspaceCreatedEvent |
tenantAggregate.TenantCreatedEvent | TenantCreatedEvent |
tenantAggregate.TenantUpdatedEvent | TenantUpdatedEvent |
tenantAggregate.TenantRemovedEvent | TenantRemovedEvent |
identityAggregate.IdentityProfileUpdatedEvent | IdentityProfileUpdatedEvent |
identityAggregate.IdentityRemovedEvent | IdentityRemovedEvent |
identityAggregate.IdentityCreatedEvent | IdentityCreatedEvent |
assetAggregate.AssetAddedEvent | AssetAddedEvent |
assetAggregate.AssetAttributeRemovedEvent | AssetAttributeRemovedEvent |
assetAggregate.AssetAttributeSetEvent | AssetAttributeSetEvent |
assetAggregate.AssetRemovedEvent | AssetRemovedEvent |
assetAggregate.AssetUpdatedEvent | AssetUpdatedEvent |
Custom Permissions
| Name | Type | Comment |
|---|---|---|
| AssetCommandAdd | Command | Create and upload asset |
| AssetCommandRemove | Command | Remove existing asset |
| AssetCommandUpdate | Command | Update existing asset |
| AssetCommandSetAttribute | Command | Set single attribute for existing asset |
| AssetCommandRemoveAttribute | Command | Remove single attribute from existing asset |
| AssetQueryDownload | Query | Download existing asset |
| AssetQueryList | Query | List all assets |
| AssetQueryModel | Query | Get asset |
| AssetQueryModelByBucketObject | Query | Get asset by bucket and object name |