Introductionâ
The LinaPro core framework exposes a set of stable capability services to plugins through the capability.Services interface. These services cover the most common cross-cutting concerns in plugin development: authentication and context, configuration and resources, data and storage, plugin governance, notifications, and framework-level capabilities such as organization and tenant management.
Source plugins obtain the full service catalog through pluginhost.Services, which extends capability.Services by adding TenantFilter(), providing a separate entry point for capabilities that carry database query builders.
This service architecture follows several core design principles:
- Explicit contracts, stable boundaries. Each service has a clear contract definition (in the
contractpackage). Plugins depend only on stable contracts, not on host-internal implementations. - Plugin-scoped isolation. Configuration, cache, manifest resources, and similar services are automatically bound to the current plugin ID, preventing interference between plugins.
- Optional capabilities, safe degradation. Framework-level capabilities such as organization and tenant management automatically degrade when their providers are unavailable. Plugins check availability through
Available(). - Read-only consumption, minimal exposure. Ordinary plugins receive read-only consumer interfaces; write operations and database query builders are not exposed through
capability.Services.
Service Quick Referenceâ
| Category | Service | Contract Type | Description |
|---|---|---|---|
| Auth & Context | APIDoc() | contract.APIDocService | API documentation localization â parses route operation keys and translates text |
| Auth & Context | Auth() | contract.AuthService | Tenant token issuance, switching, and impersonation token management |
| Auth & Context | BizCtx() | contract.BizCtxService | Reads business context snapshots for the current request â user, tenant, impersonation state, etc. |
| Auth & Context | I18n() | contract.I18nService | Runtime translation, request locale retrieval, and translation key search |
| Config & Resources | Config() | contract.ConfigService | Reads the current plugin's own static configuration |
| Config & Resources | HostConfig() | contract.HostConfigService | Reads the host's publicly whitelisted configuration keys |
| Config & Resources | Manifest() | contract.ManifestService | Reads original resource files under the current plugin's manifest/ directory |
| Data & Storage | Cache() | contract.CacheService | Plugin-scoped runtime cache |
| Data & Storage | Session() | contract.SessionService | Online session management â paginated queries and session kick-out |
| Data & Storage | Route() | contract.RouteService | Retrieves metadata for the current dynamic route |
| Plugin Governance | PluginLifecycle() | contract.PluginLifecycleService | Plugin lifecycle orchestration â pre-checks and notifications for tenant-level disable and delete |
| Plugin Governance | PluginState() | contract.PluginStateService | Queries plugin enablement state |
| Notifications | Notify() | contract.NotifyService | Publishes notifications to the host inbox |
| Capability Provider | Org() | orgcap.Service | Organization capability consumption â read-only projections of user department, position, etc. |
| Capability Provider | Tenant() | tenantcap.Service | Tenant capability consumption â current tenant, visibility validation, tenant switching |
| Data & Storage | TenantFilter() | contract.TenantFilterService | Injects tenant_id filter conditions into plugin-owned tables |
How to Obtain Servicesâ
Source Pluginsâ
Source plugins obtain the full service catalog through registrar.Services() during route registration, hook callbacks, and scheduled task registration:
func registerRoutes(ctx context.Context, registrar pluginhost.HTTPRegistrar) error {
services := registrar.Services()
// Access capability services through services
config := services.Config()
tenantFilter := services.TenantFilter()
i18n := services.I18n()
// ...
return nil
}
The pluginhost.Services returned by registrar.Services() embeds all 16 services from capability.Services and additionally provides TenantFilter(). In hook callbacks and scheduled task registration, services are obtained the same way via payload.Services() or registrar.Services().
Dynamic Pluginsâ
Dynamic plugins access core framework capabilities through the guest side SDK in pluginbridge. All service calls are bridged through hostServices and must be declared in plugin.yaml for authorization:
hostServices:
- service: config
methods: [get]
- service: hostConfig
methods: [get]
resources:
keys:
- workspace.basePath
- i18n.default
- service: manifest
methods: [get]
resources:
paths:
- profile.yaml
- service: cache
methods: [get, set, delete, incr, expire]
- service: i18n
methods: [translate, getLocale]
- service: notify
methods: [send]
The convenience functions provided by the guest side SDK (Config.String, Config.Duration, I18n.Translate, Manifest.Scan, etc.) are automatically translated into the corresponding hostServices calls at runtime â you do not need to declare these convenience function names as methods in plugin.yaml. For example, declaring methods: [get] is sufficient to use Config.String, Config.Bool, Config.Int, Config.Duration, Config.Scan, and all other read functions.
In the WASM entry function or controller, obtain services through the guest side capabilities:
// Read configuration via guest side SDK
endpoint, err := guestsdk.Config.String(ctx, "sync.endpoint", "")
// Read host configuration via guest side SDK
workspaceBase, err := guestsdk.HostConfig.String(ctx, "workspace.basePath", "/admin")
// Read manifest resources via guest side SDK
var profile struct {
Category string `yaml:"category"`
}
err := guestsdk.Manifest.Scan(ctx, "profile.yaml", "", &profile)
// Read translations via guest side SDK
message := guestsdk.I18n.Translate(ctx, "plugin.record.created", "Record created")
// Operate cache via hostServices
err := guestsdk.Cache.Set(ctx, "stats", "visit-count", "100", 0)
// Extract context from BridgeRequestEnvelopeV1
userID := requestEnvelope.UserID
tenantID := requestEnvelope.TenantID
For dynamic plugins, BizCtx context information is extracted from the BridgeRequestEnvelopeV1 request envelope rather than obtained through services.BizCtx(). Fields like UserID and TenantID in the request envelope are injected by the core framework before entering the WASM sandbox, and their semantics are consistent with the source plugin's BizCtxService.Current().
Code Examplesâ
The following examples demonstrate common patterns for using each capability service in both source plugins and dynamic plugins.
Reading Configurationâ
Source Pluginâ
Source plugins call services.Config() and services.HostConfig() directly:
func registerRoutes(ctx context.Context, registrar pluginhost.HTTPRegistrar) error {
services := registrar.Services()
syncEndpoint, err := services.Config().String(ctx, "sync.endpoint", "")
if err != nil {
return err
}
timeout, err := services.Config().Duration(ctx, "sync.timeout", 30*time.Second)
if err != nil {
return err
}
workspaceBase, err := services.HostConfig().String(ctx, "workspace.basePath", "/admin")
if err != nil {
return err
}
_ = syncEndpoint
_ = timeout
_ = workspaceBase
return nil
}
Dynamic Pluginâ
Dynamic plugins read through the guest side SDK and require config and hostConfig authorization in plugin.yaml:
syncEndpoint, err := guestsdk.Config.String(ctx, "sync.endpoint", "")
if err != nil {
return err
}
timeout, err := guestsdk.Config.Duration(ctx, "sync.timeout", 30*time.Second)
if err != nil {
return err
}
workspaceBase, err := guestsdk.HostConfig.String(ctx, "workspace.basePath", "/admin")
if err != nil {
return err
}
Cache Operationsâ
Source Pluginâ
Source plugins operate the cache through services.Cache():
func (s *recordService) GetVisitCount(ctx context.Context, recordID int) (int, error) {
services := s.services
val, err := services.Cache().Get(ctx, "visit", fmt.Sprintf("record:%d", recordID))
if err != nil {
return 0, err
}
count, _ := strconv.Atoi(val)
return count, nil
}
func (s *recordService) IncrVisitCount(ctx context.Context, recordID int) error {
services := s.services
_, err := services.Cache().Incr(ctx, "visit", fmt.Sprintf("record:%d", recordID))
return err
}
func (s *recordService) CacheSummary(ctx context.Context, recordID int, summary string) error {
services := s.services
return services.Cache().Set(ctx, "summary", fmt.Sprintf("record:%d", recordID), summary, 10*time.Minute)
}
Dynamic Pluginâ
Dynamic plugins operate the cache through hostServices with cache authorization declared in plugin.yaml:
func GetVisitCount(ctx context.Context, recordID int) (int, error) {
val, err := guestsdk.Cache.Get(ctx, "visit", fmt.Sprintf("record:%d", recordID))
if err != nil {
return 0, err
}
count, _ := strconv.Atoi(val)
return count, nil
}
func IncrVisitCount(ctx context.Context, recordID int) error {
_, err := guestsdk.Cache.Incr(ctx, "visit", fmt.Sprintf("record:%d", recordID))
return err
}
I18n Translationâ
Source Pluginâ
Source plugins translate text through services.I18n():
func (s *recordService) CreateRecord(ctx context.Context, req *CreateRecordReq) error {
services := s.services
// Translation key "plugin.record.created" defined in plugin language pack manifest/i18n/zh-CN/plugin.json
msg := services.I18n().Translate(ctx, "plugin.record.created", "Record created")
// Use translated text when publishing notifications
return services.Notify().SendNoticePublication(ctx, &contract.NoticePublication{
SourceType: "plugin",
Title: msg,
Content: fmt.Sprintf("Record ID: %d", req.ID),
})
}
Dynamic Pluginâ
Dynamic plugins translate through the guest side SDK and require service: i18n authorization in plugin.yaml:
func CreateRecord(ctx context.Context, req *CreateRecordReq) error {
msg := guestsdk.I18n.Translate(ctx, "plugin.record.created", "Record created")
// Publish notification
return guestsdk.Notify.SendNoticePublication(ctx, ¬ify.Publication{
SourceType: "plugin",
Title: msg,
Content: fmt.Sprintf("Record ID: %d", req.ID),
})
}
Publishing Notificationsâ
Source Pluginâ
Source plugins publish notifications through services.Notify():
func (s *recordService) NotifyRecordCreated(ctx context.Context, record *Record) error {
services := s.services
title := services.I18n().Translate(ctx, "plugin.record.created.title", "New Record")
content := services.I18n().Translate(
ctx,
"plugin.record.created.content",
fmt.Sprintf("Record \"%s\" has been created", record.Title),
)
return services.Notify().SendNoticePublication(ctx, &contract.NoticePublication{
SourceType: "plugin",
CategoryCode: "record_created",
Title: title,
Content: content,
})
}
Dynamic Pluginâ
Dynamic plugins publish notifications through the guest side SDK and require service: notify authorization in plugin.yaml:
func NotifyRecordCreated(ctx context.Context, record *Record) error {
title := guestsdk.I18n.Translate(ctx, "plugin.record.created.title", "New Record")
content := guestsdk.I18n.Translate(
ctx,
"plugin.record.created.content",
fmt.Sprintf("Record \"%s\" has been created", record.Title),
)
return guestsdk.Notify.SendNoticePublication(ctx, ¬ify.Publication{
SourceType: "plugin",
CategoryCode: "record_created",
Title: title,
Content: content,
})
}
Tenant-Aware Data Operationsâ
Source Pluginâ
Source plugins implement tenant-aware data operations through TenantFilterService. This service is extended in pluginhost.Services and is not part of capability.Services. The DAO layer is generated by make dao, providing dao.Xxx singletons and do.Xxx / entity.Xxx model types.
Injecting TenantFilterService in the service layer:
type serviceImpl struct {
tenantFilter plugincontract.TenantFilterService
}
func New(tenantFilter plugincontract.TenantFilterService) Service {
return &serviceImpl{tenantFilter: tenantFilter}
}
Obtain and inject it in the plugin registration entry:
services := registrar.Services()
recordSvc := recordsvc.New(services.TenantFilter())
Querying lists â use Apply() to append tenant_id conditions:
func (s *serviceImpl) List(ctx context.Context, pageNum, pageSize int) ([]*entitymodel.Record, int, error) {
model := s.tenantFilter.Apply(ctx, dao.Record.Ctx(ctx), "")
total, err := model.Count()
if err != nil {
return nil, 0, err
}
var items []*entitymodel.Record
err = model.OrderDesc(dao.Record.Columns().UpdatedAt).
Page(pageNum, pageSize).
Scan(&items)
if err != nil {
return nil, 0, err
}
return items, total, nil
}
For join queries, use the qualifier parameter to add a table qualifier to the tenant_id column, avoiding column name ambiguity:
func (s *serviceImpl) ListWithAuthor(ctx context.Context) ([]*RecordWithAuthor, error) {
model := dao.Record.Ctx(ctx).
LeftJoin("sys_user u", "u.id = "+dao.Record.Columns().AuthorId)
// qualifier is "r", generating r.tenant_id condition
model = s.tenantFilter.Apply(ctx, model, "r")
var items []*RecordWithAuthor
err := model.Scan(&items)
return items, err
}
Creating records â obtain the tenant ID through Context() and write it explicitly:
func (s *serviceImpl) Create(ctx context.Context, in *CreateRecordInput) (int64, error) {
tenantID := s.tenantFilter.Context(ctx).TenantID
recordID, err := dao.Record.Ctx(ctx).Data(do.Record{
TenantId: tenantID,
Title: strings.TrimSpace(in.Title),
Content: in.Content,
}).InsertAndGetId()
if err != nil {
return 0, err
}
return recordID, nil
}
Updating and deleting â manually add TenantFilterColumn conditions alongside the primary key to scope operations:
func (s *serviceImpl) Update(ctx context.Context, in *UpdateRecordInput) error {
tenantID := s.tenantFilter.Context(ctx).TenantID
_, err := dao.Record.Ctx(ctx).
Where(plugincontract.TenantFilterColumn, tenantID).
Where(do.Record{Id: in.Id}).
Data(do.Record{
Title: strings.TrimSpace(in.Title),
Content: in.Content,
}).
Update()
return err
}
func (s *serviceImpl) Delete(ctx context.Context, id int64) error {
tenantID := s.tenantFilter.Context(ctx).TenantID
_, err := dao.Record.Ctx(ctx).
Where(plugincontract.TenantFilterColumn, tenantID).
Where(do.Record{Id: id}).
Delete()
return err
}
Dynamic Pluginâ
Dynamic plugins work differently â due to sandbox permission controls, they perform database queries through hostServices with data authorization. Tenant filtering is automatically injected by the core framework at the data access layer:
# plugin.yaml
hostServices:
- service: data
methods: [list, get, create, update, delete]
resources:
tables:
- plugin_demo_dynamic_record
Example database operations in a dynamic plugin:
type recordDAO struct{}
func (d *recordDAO) ListByTenant(ctx context.Context, page, pageSize int) ([]*Record, error) {
result, err := guestsdk.Data.List(ctx, &data.ListRequest{
Table: "plugin_demo_dynamic_record",
Page: page,
PageSize: pageSize,
})
if err != nil {
return nil, err
}
var records []*Record
if err := result.Scan(&records); err != nil {
return nil, err
}
return records, nil
}
Related Contentâ
APIDocService
how route operation keys are resolved into localized module labels and action summaries, and where the service fits in the request pipeline for audit logs and operation records.
AuthService
the two-phase authentication model, impersonation token governance boundaries, and the service's role in multi-tenant authentication flows for tenant token issuance, switching, and impersonation.
BizCtxService
the read-only business context projection service, CurrentContext field semantics, and the service's role in the request pipeline for reading user, tenant, and impersonation state.
CacheService
plugin-scoped cache isolation mechanisms, cache value types, expiration policies, and design constraints for data isolation and safe degradation across multiple plugins and tenants.
ConfigService and HostConfigService
how plugin configuration and host configuration are read with different priority layers and whitelist boundaries, and the design constraints that keep them isolated.
I18nService
the runtime translation service, translation key naming conventions, and the relationship between translation lookup and plugin language packs.
ManifestService
path semantics for manifest resources, the boundary between resource reading and the configuration pipeline, and how to read raw resource files from the manifest/ directory.
NotifyService
the publish model for notifications, the SourceType and CategoryCode classification design, and where the service sits in the messaging pipeline.
Org (Organization Capability)
the read-only consumer interface design, Provider extension mechanism, capability degradation strategy, and its role in organization-scoped data permissions â helping plugin developers understand how to consume and extend organization capabilities.
PluginLifecycleService
its role in tenant-level lifecycle orchestration, the distinction from pluginhost.Lifecycle, and design constraints â helping plugin developers understand how the host orchestrates cross-plugin tenant-level lifecycle events.
PluginStateService
the local snapshot and authoritative read strategies for plugin enablement queries, the independent semantics of provider enablement status, and design constraints â helping plugin developers understand how to correctly query plugin enablement status.
RouteService
the design positioning of the dynamic route metadata service, the DynamicRouteMetadata struct, and its relationship with dynamic plugins â helping plugin developers understand how to retrieve metadata for the current dynamic route request.
SessionService
the online session management service covering its design positioning, Session projection model, and role in session governance, helping plugin developers understand how to query and manage online user sessions.
Tenant (Tenant Capability)
the consumer-side interface design, Provider and Resolver extension mechanisms, capability degradation strategy, and the service's role in multi-tenant architecture, helping plugin developers understand how to consume and extend tenant capabilities.
TenantFilterService
the database query injection pattern for tenant filtering, its relationship with pluginhost.Services, and its role in multi-tenant data isolation, helping plugin developers understand how to inject tenant filter conditions for plugin-owned tables.