Skip to main content
Version: 0.4.x(Latest)

Overview​

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

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

Capability Phase: Runtime

Supported Types: Source plugins, dynamic plugins

Capability Design​

SPI Pattern​

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

Graceful Degradation​

When no tenant provider is available, the system degrades to single-tenant mode for the platform tenant. The standard 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 flows.

Source Plugin Exclusive: TenantFilter​

Source plugins that need to query plugin-owned tables can obtain tenantcap.PluginTableFilterService through pluginhost.Services.TenantFilter(). It belongs to the tenant domain capability but is not part of the standard capability.Services.Tenant() because it directly receives and returns *gdb.Model query builders, making it suitable only for source plugins running inside the host process.

MethodDescription
ContextReturns the current request's tenant, user, real operator, impersonation state, and platform bypass information
ApplyAppends a tenant_id condition to the query model; returns the original model when the current request allows platform bypass

TenantFilterContext contains UserID, TenantID, ActingUserID, OnBehalfOfTenantID, ActingAsTenant, IsImpersonation, and PlatformBypass. Among these, ActingUserID is suitable for writing audit records, while PlatformBypass is determined by host policy and plugins should not construct it themselves.

Dynamic plugins do not use TenantFilter(). When dynamic plugins access plugin-owned tables, they should declare service: data and authorized resources.tables, with the host data service handling tenant, authorization, and table namespace governance.

Interface Definitions​

Source Plugin Interface​

MethodDescription
AvailableChecks whether the tenant capability has an available provider
StatusReturns capability status, active provider, and conflict reasons
CurrentReturns the current request tenant; returns the platform tenant when missing
PlatformBypassChecks whether the current request allows bypassing tenant filtering
EnsureTenantVisibleValidates that the current user can access the specified tenant
ValidateUserInTenantValidates that the specified user belongs to the specified tenant
ListUserTenantsLists active tenants visible to the user
SwitchTenantValidates that a tenant switch target is legal

Source plugin exclusive interface (accessed through pluginhost.Services.TenantFilter()):

MethodDescription
ContextReturns the current request's tenant context information
ApplyAppends a tenant_id condition 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
tenants.platform_bypassChecks whether bypassing tenant filtering is allowed
tenants.visible.ensureValidates that the current user can access the specified tenant
users.tenant_membership.validateValidates that the specified user belongs to the specified tenant
users.tenants.listLists active tenants visible to the user
tenants.switch.validateValidates that a tenant switch target is legal

Usage​

Source Plugin Usage​

Source plugins access the standard tenant capability through services.Tenant():

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

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

// Validate tenant visibility
err := services.Tenant().EnsureTenantVisible(ctx, targetTenantID)

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

Source plugins use TenantFilter() to append tenant filtering to plugin-owned tables:

model := g.DB().Model("plugin_record")
model = services.TenantFilter().Apply(ctx, model, "")
result, err := model.Where("status", "active").All()

The third parameter of Apply is a table name or alias qualifier:

qualifierResult
Empty stringUses tenant_id
plugin_recordUses plugin_record.tenant_id
rUses r.tenant_id

Dynamic Plugin Usage​

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

hostServices:
- service: tenant
methods:
- tenants.current
- tenants.visible.ensure
- users.tenants.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
tenant := tenantSvc.Current(ctx)

// Validate tenant visibility
err := tenantSvc.EnsureTenantVisible(ctx, targetTenantID)

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

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

Design Constraints​

  • Capability is optional. When no tenant provider is available, the system degrades to single-tenant mode for the platform tenant.
  • Query filtering is not in the standard service. Tenant scope capabilities requiring database query builders belong to the host-internal ScopeService or the source-plugin-exclusive TenantFilter().
  • TenantFilter() is only for plugin-owned tables. Do not use it to operate on host core tables, and do not hand-write inconsistent tenant conditions in plugin code.
  • Pass qualifiers for joined queries. When multiple tables contain tenant_id, pass the table name or alias to avoid column name ambiguity.
  • Tenant switching only validates. SwitchTenant validates target legality; token re-issuance is handled by Auth().Token().SwitchTenant.
  • Platform bypass is determined by the host. Plugins should not construct cross-tenant access states themselves.