Skip to main content
Version: 0.4.x

Overview​

Source-code plugins consume the tenant capability through services.Tenant(). The tenant capability is an optional framework capability, with the official provider plugin identified as linapro-tenant-core. When no active provider is available, the service degrades to single-tenant semantics under platform tenant 0.

The Tenant capability uses a sub-service model, aggregating Context() (current tenant context), Directory() (tenant directory), Membership() (user-tenant membership), Plugins() (tenant plugin governance), and Filter() (tenant filter context).

Dynamic plugins can declare service: tenant to call published tenant capability methods.

Capability Phase: Runtime

Supported Types: Source-code plugins, dynamic plugins

Capability Design​

SPI Pattern​

The Tenant capability uses an SPI pattern, where the specific multi-tenant strategy is implemented by the provider plugin. tenantcap.Provider is responsible for tenant resolution, user-tenant relationship validation, user-visible tenant lists, and tenant-switch validation. tenantcap.Resolver resolves tenant identity from HTTP requests and can form a chain of responsibility based on request headers, domains, paths, tokens, or other strategies.

Sub-Service Architecture​

The Tenant capability uses a sub-service model, handling different tenant domain operations separately:

Graceful Degradation​

When no tenant provider is available, the system degrades to single-tenant mode under the platform tenant. Tenant() does not expose RequestResolver, ScopeService, user-tenant membership writes, or startup consistency checks -- these belong to the host's internal middleware, database filtering, or governance processes.

Interface Definitions​

Source-Code Plugin Interface​

Tenant() root methods:

MethodDescription
AvailableChecks whether the tenant capability has an available provider
StatusReturns capability status, active provider, and conflict reason
Context()Returns the current tenant context sub-service
Directory()Returns the tenant directory sub-service
Membership()Returns the user-tenant membership sub-service
Plugins()Returns the tenant plugin governance sub-service
Filter()Returns the tenant filter context sub-service

Tenant().Context() sub-service:

MethodDescription
CurrentReturns the current request tenant identifier; falls back to platform tenant when absent
InfoReturns current request tenant information, including ID, Code, Name, and Status
PlatformBypassChecks whether the current request is allowed to bypass tenant filtering

Tenant().Directory() sub-service:

MethodDescription
GetRetrieves a single visible tenant's information
BatchGetBatch-retrieves visible tenant information, returning BatchResult
ListSearches visible tenant candidates by keyword
EnsureVisibleValidates whether the current user can access the specified tenant set

Tenant().Membership() sub-service:

MethodDescription
ListByUserLists the active tenants visible to a user
ValidateValidates whether a specified user belongs to a specified tenant

Tenant().Plugins() sub-service:

MethodDescription
SetTenantPluginEnabledUpdates tenant plugin enabled status, subject to caller and tenant policy validation
ProvisionTenantPluginDefaultsCreates missing default plugin rows for a tenant

Tenant().Filter() sub-service (source-code plugins only):

MethodDescription
ContextReturns the current request's tenant, user, real actor, impersonation state, and platform bypass information

Source-code plugin exclusive interface (accessed via pluginhost.Services.TenantFilter()):

MethodDescription
ContextReturns the current request's tenant context information
ApplyAppends tenant_id conditions to the query model

Dynamic Plugin Interface​

Dynamic MethodDescription
capability.availableChecks whether the tenant capability has an available provider
capability.statusReturns capability status and active provider
tenants.currentReturns the current request tenant identifier
tenants.current_infoReturns the current request tenant information
tenants.platform_bypassChecks whether tenant filtering can be bypassed
tenants.visible.ensureValidates whether the current user can access the specified tenant
tenants.batch_getBatch-retrieves visible tenant information
tenants.searchSearches visible tenant candidates by keyword
tenants.visible.batch_ensureBatch-validates whether the current user can access the specified tenants
users.tenant_membership.validateValidates whether a specified user belongs to a specified tenant
users.tenants.listLists the active tenants visible to a user
users.tenants.batch_listBatch-retrieves user-accessible tenant lists
tenants.switch.validateValidates whether a tenant-switch target is valid

Usage​

Source-Code Plugin Usage​

Source-code plugins access general tenant capabilities through services.Tenant():

// Check if tenant capability is available
if !services.Tenant().Available(ctx) {
// Handle degradation
return
}

// Get the current tenant identifier
tenantID := services.Tenant().Context().Current(ctx)

// Get the current tenant information
tenantInfo, err := services.Tenant().Context().Info(ctx)

// Check platform bypass
bypass := services.Tenant().Context().PlatformBypass(ctx)

// Validate tenant visibility
err := services.Tenant().Directory().EnsureVisible(ctx, []tenantcap.TenantID{targetTenantID})

// List user-visible tenants
tenants, err := services.Tenant().Membership().ListByUser(ctx, userID)

// Batch-retrieve tenant information
batchResult, err := services.Tenant().Directory().BatchGet(ctx, tenantIDs)

// Search tenant candidates
page, err := services.Tenant().Directory().List(ctx, tenantcap.ListInput{
Keyword: "tech",
Page: pageRequest,
})

// Validate user-tenant membership
err := services.Tenant().Membership().Validate(ctx, userID, targetTenantID)

Source-code plugins obtain the tenant filter context through Filter().Context() to build their own query conditions:

filterCtx := services.Tenant().Filter().Context(ctx)
if filterCtx.TenantID > 0 {
model = model.Where("tenant_id", filterCtx.TenantID)
}
if filterCtx.IsImpersonation {
// Log impersonation audit
log.Infof("Impersonated user %d accessing tenant %d", filterCtx.ActingUserID, filterCtx.TenantID)
}

Dynamic Plugin Usage​

Dynamic plugins declare the tenant service and authorized methods in plugin.yaml:

hostServices:
- service: tenant
methods:
- tenants.current
- tenants.current_info
- tenants.visible.ensure
- tenants.batch_get
- tenants.search
- users.tenants.list
- users.tenants.batch_list

tenant is a none resource type and does not declare paths, tables, keys, or resources. Usage on the dynamic plugin side:

tenantSvc := pluginbridge.Default().Tenant()

// Get the current tenant identifier
tenantID := tenantSvc.Context().Current(ctx)

// Get the current tenant information
tenantInfo, err := tenantSvc.Context().Info(ctx)

// Validate tenant visibility
err := tenantSvc.Directory().EnsureVisible(ctx, []tenantcap.TenantID{targetTenantID})

// List user-visible tenants
tenants, err := tenantSvc.Membership().ListByUser(ctx, userID)

// Batch-retrieve tenant information
batchResult, err := tenantSvc.Directory().BatchGet(ctx, tenantIDs)

// Search tenant candidates
page, err := tenantSvc.Directory().List(ctx, tenantcap.ListInput{
Keyword: "tech",
Page: pageRequest,
})

When dynamic plugins access plugin-owned tables, they should declare service: data and authorized resources.tables, with the host data service enforcing tenant boundary governance.

Design Constraints​

  • Capability is optional. When no tenant provider is available, the system degrades to single-tenant mode under the platform tenant.
  • Query filtering is not in the general service. Tenant-scoped capabilities that require a database query builder belong to the host's internal ScopeService.
  • Filter().Context() returns a read-only context. Plugins build their own query conditions based on the returned TenantFilterContext and should not use it to manipulate host core tables.
  • Tenant switching is validation only. Membership().Validate validates the target's legitimacy; re-issuing tokens is still handled by Auth().Token().SwitchTenant.
  • Platform bypass is determined by the host. Plugins should not construct cross-tenant access states on their own.