Files
+1 98652c6476 Include sub projects (#10201)
* Add bump-changes

* Add utility tests

* Add utility tests

* Bump to new version of esbuild and typescript

* v0.7.3

* use platform rig 0.7.10

* upgrade: memory engine optimized; change name  to  (was recommended by Copilot and Onnikov, TODO: CHANGE CLIENT TOO!!!)

Signed-off-by: Leonid Kaganov <lleo@lleo.me>

* Fix rate limits bug

* Bump versions

* Fix lock file

* Fix bug in queue cleanup

* Add more tests for queue

* Add api-test tests

* Initial commit

* Improve hierarchy + tests

Add tests for hierarchy and few performance/memory  optimizations.

* Add more hierarchy tests

* Move from Huly platform repository

* Add docker tests setup

* Fix test to be executed only once

* Add connection tests

* Fix package include source files

* More tests

* Create README.md

* Fix pnpm lock

* Fix packages publish

* Remove broken tests

* feat: adjust hulylake client for storage adapter

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* Fix export

* Fix publish

* Fix message update (#114)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>

* Bump version

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>

* Bump versions

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* Fix lang store (#115)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>

* update hulylake client

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* Bump version

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* Add hulylake storage adapter

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* Bump version

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* fix validation issues

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* fix: do not fail on deseralization error and add logs

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* bump version -> 0.1.14

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* fix collaboration test

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* Update prettier and new update-deps script

Prettier + svelte support

* fix unstable ydoc tests

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* Add tx ordering middleware

* Fix ordering tests

* Fix Kafka close of admin

* Add tests for measurement and understand overhead

* Fix not updated lock file

* Fix update-deps

* Fix update-deps

* Use latest platform-rig

* Fix deps

* Add rush check to CI

* Use latest versions

* Bump versions

* Fix lock file

* validate json patch

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* bump version -> 0.1.15

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* fix merge unit tests

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* Script to sync eslint deps

* Fix deps

* Fix tests

* Fix platform-rig detection

* Update to latest platform-rig

* Update to latest platform rig and core

* Bump typescript

* Bump typescript

* Rollback eslint plugins

* Fix lock file

* Bump platform-rig

* Update to latest platform-rig

* update to latest platform-rig

* Allow to compile svelte files

* Add ui-test component for checking compile

* Fix log levels rename compile ui -> compile ui-esbuild

* Fix build

* Bump esbuild svelte version

* Chore: use fixed versions in update-deps

Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>

* Chore: commit changes

Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>

* Update deps

* Add tests for session manager

* Fix txOrdering implementation

* Bump ordering

* Prevent metrics zero values in measure

+ Fix format svelte files

* Revert update-deps script logic

* v0.7.19

* update to latest platform-rig

* Update deps

* Fix pnpm

* Session counters

* Fix pnpm lock

* Add storage client

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* Bump versions

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* fix versions

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* Bump core

* Fix pnpm

* Get rid of communication dependency

* Add copilot memory file

* Use proper name for instructions file

* Fix instructions

* Use domain instead of test name in gauges

* Update instructions file

* fix front service upload

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* remove incorrect test

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* Move packages to huly.core

* Move packages to core, since they are not utils

* Add global user profile

Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>

* Fix lock file

Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>

* Add support for memory limit check

* Bump version

* Fix pnpm

* report more accurate upload progress

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* fic validation issues

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* Fix deps

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>

* Move LowLevelStorage to server

* Fix linting

* Revert "Fix linting"

This reverts commit 54631d353e.

* Revert "Move LowLevelStorage to server"

This reverts commit aafb8f6f12.

* feature: add regorus engine with permit file

Signed-off-by: Leonid Kaganov <lleo@lleo.me>

* Fix one second counters for memory usage

* Fix kafka test

* use fresh core

* Version bump

* fix: key parameter added

Signed-off-by: Leonid Kaganov <lleo@lleo.me>

* Fix readme and few author mistakes

* Export domain schemas

* Bump version

* Tests (#117)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>

* Bump version

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>

* feat: compact compact worker (#4)

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* bump version -> 0.1.16

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* Add TypeIdentifier

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>

* Add change logs

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>

* rename send -> try_send

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* Bump

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>

* Fix pnpm lock

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>

* Add identifier middleware, bump core

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>

* Add subsciption methods to account client

Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>

* Fix lock file

Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>

* Fix reaction notification (#118)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>

* Bump version

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>

* Improve find methods schemas to convert to valid types

Signed-off-by: Nikolay Marchuk <nikolay.marchuk@hardcoreeng.com>

* Add change description

Signed-off-by: Nikolay Marchuk <nikolay.marchuk@hardcoreeng.com>

* Do not transcode while recording

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* Open telemetry support

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* use proper content type in multipart upload

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* Bump versions

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* fix build (#26)

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* Fix peers (#120)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>

* Bump version

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>

* Add ActivityCollaborativeChange

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>

* Update pnpm

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>

* Allow to suspend errors on with

* Fix pnpm cache

* update versions

* v0.7.17 for all

* v0.7.11

* v0.7.14

* remove arc from worker

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* bump version -> 0.1.17

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* Rank for attributes

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>

* Update pnpm

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>

* Fix one second counters

* Fix withContext and allow pass options

* Fix formatting

* Use updated deps

* Bump versions

* Update deps

* Update deps to platform.core

* add support for textColor mark

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* add support for textStyle mark

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* Bump versions

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* Bump versions again

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* Fix

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>

* Rework on second timers

* fix merge of large blobs feched from s3

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* bump version -> 0.1.18

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* New subscription methods in account-client

* Update lock file

Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>

* Send error on find for wrong domain

* Suspend connect custom errors events in traces

* Bump client

* Bump core

* update deps

* Fix lock file

* Sorting for TypeIdentifier

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>

* Bump version

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>

* add workspace usage info

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* Bump versions

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>

* Improve pg security perfomance

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>

* Fix identifier middleware

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>

* Update TxAccessLevel interface

Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>

* Allow guest to update its identities

Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>

* Add password login locked platform status

Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>

* Fix Uptrace normalizeMarkdown errors

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* Add change log

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* Add txMatch to permission

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>

* update pnpm lock

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>

* Bump

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>

* Fix permission middleware

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>

* Fix enum sorting

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>

* Enable formatting check

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>

* Enable formatting check

* Add change

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>

* Fix Uptrace NaN error

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* feature: removed actors, improved performance

Signed-off-by: Leonid Kaganov <lleo@lleo.me>

* feature: ping from server to clients added

Signed-off-by: Leonid Kaganov <lleo@lleo.me>

* feature: ping from server to clients added

Signed-off-by: Leonid Kaganov <lleo@lleo.me>

* Compress kafka messages and fix exception in findAll

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* Bump versions

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* Bump versions

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* Rush change

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* Fix compression param

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* Trigger change

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* Clean up

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* Trigger change

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* Bump markdown version

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* Enable sub projects

* Fix wrong double symbol scripts

* Include foundation packages

* Add support for custom exclude filters

Add support for custom exclude filters - by Andrey Sobolev - haiodo@gmail.com

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>

* Bump

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>

* Fix Uptrace filter is not a function error

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* Sync versions

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>

---------

Signed-off-by: Leonid Kaganov <lleo@lleo.me>
Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>
Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
Signed-off-by: Nikolay Marchuk <nikolay.marchuk@hardcoreeng.com>
Signed-off-by: Artem Savchenko <armisav@gmail.com>
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
Co-authored-by: Leonid Kaganov <lleo@lleo.me>
Co-authored-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
Co-authored-by: Alexander Onnikov <Alexander.Onnikov@gmail.com>
Co-authored-by: Kristina <kristin.fefelova@gmail.com>
Co-authored-by: Alexey Zinoviev <alexey.zinoviev@xored.com>
Co-authored-by: Denis Bykhov <bykhov.denis@gmail.com>
Co-authored-by: Nikolay Marchuk <nikolay.marchuk@hardcoreeng.com>
Co-authored-by: Alexander Onnikov <aonnikov@hardcoreeng.com>
Co-authored-by: Artem Savchenko <armisav@gmail.com>
2025-11-26 19:15:30 +05:00

644 lines
16 KiB
Markdown

# Multi-Tenant Architectures with Huly Network
Building secure, scalable multi-tenant applications using Huly Virtual Network.
## Table of Contents
- [Introduction](#introduction)
- [Multi-Tenancy Patterns](#multi-tenancy-patterns)
- [Container Isolation](#container-isolation)
- [Tenant Identification](#tenant-identification)
- [Data Isolation](#data-isolation)
- [Resource Management](#resource-management)
- [Security Considerations](#security-considerations)
- [Billing and Metering](#billing-and-metering)
- [Complete Example](#complete-example)
## Introduction
Multi-tenancy allows multiple customers (tenants) to share the same infrastructure while maintaining complete data and security isolation. Huly Network provides natural multi-tenancy through container kinds, labels, and reference management.
### Benefits of Multi-Tenant Architecture
- **Cost Efficiency**: Share infrastructure costs across tenants
- **Scalability**: Scale resources per tenant independently
- **Isolation**: Complete data and security separation
- **Flexibility**: Different tiers and features per tenant
- **Maintainability**: Single codebase for all tenants
## Multi-Tenancy Patterns
### Pattern 1: Tenant-Per-Container
Each tenant gets dedicated container instances:
```typescript
// Request workspace for specific tenant
const workspace = await client.get('workspace' as ContainerKind, {
labels: ['tenant-acme-corp']
})
```
**Pros:**
- Complete isolation
- Easy to track resource usage
- Can scale per tenant
**Cons:**
- More containers to manage
- Higher resource overhead
### Pattern 2: Shared Container with Tenant Filtering
Multiple tenants share containers, data filtered by tenant ID:
```typescript
class SharedWorkspaceContainer implements Container {
async request(operation: string, data?: any, clientId?: ClientUuid): Promise<any> {
const tenantId = data.tenantId
// All operations scoped to tenant
switch (operation) {
case 'getData':
return this.getData(tenantId, data.filter)
}
}
private async getData(tenantId: string, filter: any): Promise<any> {
// Query with tenant filter
return this.db.query({ ...filter, tenantId })
}
}
```
**Pros:**
- Fewer containers
- Better resource utilization
- Easier to manage
**Cons:**
- Must ensure data isolation in code
- Risk of data leakage if not careful
### Pattern 3: Hybrid Approach
Shared containers for common operations, dedicated for sensitive data:
```typescript
// Shared query engine
const queryEngine = await client.get('query-engine' as ContainerKind, {
labels: ['shared']
})
// Dedicated workspace for sensitive operations
const workspace = await client.get('workspace' as ContainerKind, {
labels: ['tenant-acme-corp', 'dedicated']
})
```
## Container Isolation
### Using Labels for Tenant Routing
```typescript
// Agent factory with tenant-aware containers
// Note: For production code, use serveAgent() on the client
import { createNetworkClient } from '@hcengineering/network-client'
const client = createNetworkClient('localhost:3737')
await client.waitConnection(5000)
await client.serveAgent('localhost:3738', {
'tenant-workspace': async (options: GetOptions) => {
const tenantId = options.labels?.[0]
if (!tenantId) {
throw new Error('Tenant ID required')
}
const uuid = `workspace-${tenantId}-${Date.now()}` as ContainerUuid
const container = new TenantWorkspaceContainer(uuid, tenantId)
return {
uuid,
container,
endpoint: `workspace://${tenantId}/${uuid}` as any
}
}
})
```
### Tenant Workspace Container
```typescript
export class TenantWorkspaceContainer implements Container {
private data = new Map<string, any>()
private users = new Set<string>()
constructor(readonly uuid: ContainerUuid, readonly tenantId: string) {
console.log(`Workspace created for tenant: ${tenantId}`)
}
async request(operation: string, data?: any): Promise<any> {
// All operations automatically scoped to this.tenantId
switch (operation) {
case 'createDocument':
return this.createDocument(data)
case 'listDocuments':
return this.listDocuments()
case 'addUser':
return this.addUser(data.userId)
}
}
private async createDocument(data: any): Promise<any> {
const docId = generateId()
const doc = {
id: docId,
tenantId: this.tenantId, // Automatically tagged
...data,
createdAt: Date.now()
}
this.data.set(docId, doc)
await this.broadcast({
type: 'documentCreated',
document: doc
})
return { success: true, document: doc }
}
private async listDocuments(): Promise<any> {
// Only returns documents for this tenant
return {
success: true,
documents: Array.from(this.data.values()),
tenantId: this.tenantId
}
}
}
```
## Tenant Identification
### Method 1: Labels
Use labels to identify tenants:
```typescript
// Client requests workspace for tenant
const workspace = await client.get('workspace' as ContainerKind, {
labels: ['tenant-id:acme-corp', 'tier:enterprise']
})
```
### Method 2: Extra Parameters
Pass tenant context in extra parameters:
```typescript
const workspace = await client.get('workspace' as ContainerKind, {
extra: {
tenantId: 'acme-corp',
tier: 'enterprise',
region: 'us-west'
}
})
```
### Method 3: Request-Time Identification
Include tenant in every request:
```typescript
await workspace.request('createDocument', {
tenantId: 'acme-corp', // Required in every request
title: 'Q1 Report',
content: '...'
})
```
### Best Practice: Combined Approach
```typescript
// Container-level isolation (preferred)
const workspace = await client.get('workspace' as ContainerKind, {
labels: ['tenant:acme-corp']
})
// Request-level validation (defense in depth)
await workspace.request('createDocument', {
tenantId: 'acme-corp', // Validated against container's tenant
data: { ... }
})
```
## Data Isolation
### Database-Level Isolation
```typescript
export class DatabaseContainer implements Container {
private db: TenantDatabase
constructor(readonly uuid: ContainerUuid, readonly tenantId: string) {
// Connect to tenant-specific database
this.db = new TenantDatabase({
database: `tenant_${tenantId}`,
schema: tenantId
})
}
async request(operation: string, data?: any): Promise<any> {
// All queries automatically scoped to tenant database
switch (operation) {
case 'query':
return await this.db.query(data.sql, data.params)
}
}
}
```
### Row-Level Security
```typescript
export class SharedDatabaseContainer implements Container {
private db: Database
async request(operation: string, data?: any): Promise<any> {
const tenantId = data.tenantId
switch (operation) {
case 'query':
// Always add tenant filter
return await this.db.query(data.sql + ' WHERE tenant_id = ?', [...data.params, tenantId])
}
}
}
```
### In-Memory Isolation
```typescript
export class CacheContainer implements Container {
// Separate cache per tenant
private caches = new Map<string, Map<string, any>>()
private getTenantCache(tenantId: string): Map<string, any> {
if (!this.caches.has(tenantId)) {
this.caches.set(tenantId, new Map())
}
return this.caches.get(tenantId)!
}
async request(operation: string, data?: any): Promise<any> {
const cache = this.getTenantCache(data.tenantId)
switch (operation) {
case 'get':
return { value: cache.get(data.key) }
case 'set':
cache.set(data.key, data.value)
return { success: true }
}
}
}
```
## Resource Management
### Resource Quotas
```typescript
interface TenantQuota {
maxDocuments: number
maxUsers: number
maxStorageBytes: number
maxRequestsPerSecond: number
}
export class QuotaEnforcedContainer implements Container {
private quotas = new Map<string, TenantQuota>()
private usage = new Map<string, TenantUsage>()
constructor(readonly uuid: ContainerUuid, readonly tenantId: string) {
// Load quota for tenant
this.quotas.set(tenantId, this.loadQuota(tenantId))
}
async request(operation: string, data?: any): Promise<any> {
// Check quota before operation
if (!(await this.checkQuota(operation, data))) {
return {
success: false,
error: 'quota_exceeded',
message: 'Your plan limit has been reached'
}
}
// Process request
const result = await this.processRequest(operation, data)
// Update usage
await this.updateUsage(operation, data)
return result
}
private async checkQuota(operation: string, data: any): Promise<boolean> {
const quota = this.quotas.get(this.tenantId)!
const usage = this.usage.get(this.tenantId) || { documents: 0, users: 0 }
switch (operation) {
case 'createDocument':
return usage.documents < quota.maxDocuments
case 'addUser':
return usage.users < quota.maxUsers
default:
return true
}
}
}
```
### Rate Limiting
```typescript
import { RateLimiter } from 'limiter'
export class RateLimitedContainer implements Container {
private limiters = new Map<string, RateLimiter>()
constructor(readonly uuid: ContainerUuid, readonly tenantId: string) {
// Different limits per tier
const tier = this.getTenantTier(tenantId)
const limit = tier === 'enterprise' ? 1000 : 100 // requests per second
this.limiters.set(
tenantId,
new RateLimiter({
tokensPerInterval: limit,
interval: 'second'
})
)
}
async request(operation: string, data?: any): Promise<any> {
const limiter = this.limiters.get(this.tenantId)!
// Try to consume token
if (!(await limiter.tryRemoveTokens(1))) {
return {
success: false,
error: 'rate_limit_exceeded',
message: 'Too many requests, please try again later'
}
}
return await this.processRequest(operation, data)
}
}
```
## Security Considerations
### Preventing Data Leakage
```typescript
export class SecureTenantContainer implements Container {
async request(operation: string, data?: any, clientId?: ClientUuid): Promise<any> {
// 1. Validate tenant access
if (!(await this.validateTenantAccess(data.tenantId, clientId))) {
throw new Error('Unauthorized tenant access')
}
// 2. Ensure tenant ID matches container
if (data.tenantId !== this.tenantId) {
throw new Error('Tenant ID mismatch')
}
// 3. Process request
const result = await this.processRequest(operation, data)
// 4. Sanitize response (remove cross-tenant data)
return this.sanitizeResponse(result, this.tenantId)
}
private sanitizeResponse(data: any, tenantId: string): any {
// Remove any data not belonging to tenant
if (Array.isArray(data)) {
return data.filter((item) => item.tenantId === tenantId)
}
return data
}
}
```
### Audit Logging
```typescript
export class AuditedContainer implements Container {
async request(operation: string, data?: any, clientId?: ClientUuid): Promise<any> {
const startTime = Date.now()
// Log request
await this.auditLog({
timestamp: new Date().toISOString(),
tenantId: this.tenantId,
clientId,
operation,
data: this.sanitizeForAudit(data)
})
try {
const result = await this.processRequest(operation, data)
// Log success
await this.auditLog({
timestamp: new Date().toISOString(),
tenantId: this.tenantId,
operation,
status: 'success',
duration: Date.now() - startTime
})
return result
} catch (error: any) {
// Log failure
await this.auditLog({
timestamp: new Date().toISOString(),
tenantId: this.tenantId,
operation,
status: 'error',
error: error.message,
duration: Date.now() - startTime
})
throw error
}
}
}
```
## Billing and Metering
### Usage Tracking
```typescript
export class MeteredContainer implements Container {
private usage = {
requests: 0,
computeTime: 0,
storageBytes: 0
}
async request(operation: string, data?: any): Promise<any> {
const startTime = Date.now()
this.usage.requests++
try {
const result = await this.processRequest(operation, data)
// Track compute time
const duration = Date.now() - startTime
this.usage.computeTime += duration
// Track storage if applicable
if (operation === 'store') {
this.usage.storageBytes += this.calculateSize(data)
}
// Report to billing system
await this.reportUsage()
return result
} catch (error) {
this.usage.computeTime += Date.now() - startTime
throw error
}
}
private async reportUsage(): Promise<void> {
// Send to billing system every 1000 requests
if (this.usage.requests % 1000 === 0) {
await fetch('https://billing.api/usage', {
method: 'POST',
body: JSON.stringify({
tenantId: this.tenantId,
period: new Date().toISOString(),
usage: this.usage
})
})
}
}
}
```
## Complete Example
### SaaS Application with Multiple Tenants
```typescript
// See examples/03-multi-tenant.ts for complete working example
import { AgentImpl } from '@hcengineering/network-core'
import type { GetOptions, ContainerUuid } from '@hcengineering/network-core'
// Define tenant workspace container
class SaaSTenantContainer implements Container {
private data = new Map<string, any>()
private users = new Set<string>()
private quota: TenantQuota
private usage: TenantUsage
constructor(readonly uuid: ContainerUuid, readonly tenantId: string, readonly tier: 'free' | 'pro' | 'enterprise') {
this.quota = this.getQuotaForTier(tier)
this.usage = { documents: 0, users: 0, requests: 0 }
}
async request(operation: string, data?: any): Promise<any> {
// Rate limiting
if (!(await this.checkRateLimit())) {
return { success: false, error: 'rate_limit_exceeded' }
}
// Quota checking
if (!(await this.checkQuota(operation))) {
return { success: false, error: 'quota_exceeded' }
}
// Process request
this.usage.requests++
switch (operation) {
case 'createDocument':
return await this.createDocument(data)
case 'listDocuments':
return await this.listDocuments()
case 'addUser':
return await this.addUser(data)
case 'getUsage':
return { success: true, usage: this.usage, quota: this.quota }
}
}
// Implementation details...
}
// Create agent
// Note: For production code, use serveAgent() on the client
const client = createNetworkClient('localhost:3737')
await client.waitConnection(5000)
await client.serveAgent('localhost:3738', {
'tenant-workspace': async (options: GetOptions) => {
const tenantId = options.labels?.[0]
const tier = options.extra?.tier || 'free'
const container = new SaaSTenantContainer(`workspace-${tenantId}` as ContainerUuid, tenantId, tier)
return {
uuid: container.uuid,
container,
endpoint: `saas://${tenantId}/${container.uuid}` as any
}
}
})
```
## Best Practices
1. **Always validate tenant ID** in every request
2. **Use container-level isolation** when possible
3. **Implement quotas and rate limiting** per tenant
4. **Audit all operations** for security and compliance
5. **Monitor resource usage** per tenant
6. **Test cross-tenant access** prevention
7. **Plan for tenant data migration** and export
8. **Document tenant isolation** boundaries
9. **Implement graceful degradation** when quotas exceeded
10. **Provide tenant analytics** dashboard
## Next Steps
- [Container Development Guide](CONTAINER_DEVELOPMENT.md)
- [Security Best Practices](SECURITY.md)
- [Performance Tuning](PERFORMANCE.md)
- [Examples: Multi-Tenant](../examples/03-multi-tenant.ts)
---
For help building multi-tenant applications, see the [Troubleshooting Guide](TROUBLESHOOTING.md).