Skip to main content
Version: 0.3.x(Latest)
Note

LinaPro is currently in its testing phase. The boundary designs and implementation details described here may change. This page is provided for reference only.

Design Philosophy​

LinaPro was designed around one core principle from the start: the core framework is responsible only for lightweight foundational capabilities and stable extension interfaces, while business capabilities are implemented through plugins.

The reason is straightforward. If the core framework carries too much business logic, every business change forces changes to the framework itself, raising upgrade cost and weakening stability. When business capabilities move into plugins instead, the core framework can focus on stable infrastructure: authentication, authorization, multi-tenancy, route dispatch, scheduled tasks, static assets, and plugin lifecycle management. Once these foundations are stable, they can support rapid iteration in upper-layer plugins for the long term.

That does not mean the core framework and plugins are completely isolated. Collaboration itself needs boundaries and rules. If a plugin depends too deeply on the core framework, for example by importing the core framework's internal packages directly or bypassing published interfaces to call private implementations, the plugin will break whenever core framework internals evolve. Conversely, if the core framework exposes one-off interfaces just to accommodate a specific plugin, unnecessary coupling appears between the core framework and that plugin, harming maintainability across the whole system.

LinaPro establishes clear collaboration boundaries between the core framework and plugins in three ways:

  • Source plugins integrate through the pluginhost contract: every interface in the pluginhost package is a stable commitment from the core framework to source plugins. Source plugins can use core framework capabilities only through these interfaces and must not import the core framework's internal/ directory directly.
  • Dynamic plugins communicate through the pluginbridge sandbox: dynamic plugins run inside a WASM sandbox and call core framework services through the host_call mechanism. The core framework validates every call against the hostServices authorization snapshot confirmed at installation time.
  • The core framework provides general foundations, not business-specific interfaces: pluginhost exposes general service adapters for authentication, tenancy, configuration, cache, notifications, and similar platform capabilities. It does not expose domain-specific business APIs for a particular plugin; plugins build their own business logic on top of those primitives.

This design keeps core framework stability and plugin flexibility compatible. The two sides collaborate across explicit boundaries instead of leaking into each other's implementation.

Core Framework Capability Design​

The foundational capabilities provided by the core framework lina-core can be divided into two groups: infrastructure capabilities and plugin extension interfaces.

Infrastructure Capabilities​

Infrastructure capabilities apply to the whole runtime environment. They are not designed for any single business scenario or page:

Capability AreaDescription
Authentication and sessionsJWT issuance and validation, session storage, forced sign-out, session activity refresh
Permission managementRBAC model, menu and button permissions, permission identifier enforcement middleware
Multi-tenancyTenant resolution, tenant context injection, tenant filter service
Routing and middlewareUnified response serialization, CORS, request body limits, business context injection
Scheduled tasksTask scheduling, distributed leader election, task execution logs
Static assetsEmbedded frontend build artifacts and unified plugin asset serving
ConfigurationStatic configuration reading and runtime configuration items
CachePlugin-scoped cache namespaces
Plugin governancePlugin directory scanning, dependency checks, lifecycle orchestration, runtime upgrades

These capabilities are wrapped as stable service adapters and exposed to plugins through the HostServices interface. The core framework does not define a dedicated interface for a single business scenario. Instead, it provides composable general primitives, and each plugin decides how to use them in its own logic.

Plugin Extension Interfaces​

The core framework defines two stable extension interfaces for plugins:

Source plugins use pluginhost at compile time to register routes, hooks, scheduled tasks, lifecycle callbacks, and governance logic. Dynamic plugins use pluginbridge at runtime to access core framework capabilities through sandbox communication. The two interfaces differ in capability scope, but both plugin types are governed transparently by the core framework.

Admin Workspace Responsibility Boundaries​

LinaPro includes a default admin workspace built on Vue 3 + Vben5. By default, it is served under the /admin route. The workspace provides visual management for system modules and is the standard UI expression layer for core framework APIs and plugin APIs.

/admin is the entry boundary of the default admin workspace, not the boundary of all frontend capability in the system. The workspace SPA static asset service and refresh fallback serve only /admin and its subpaths. They do not occupy the root path / by default. Therefore, the root path, portal pages, plugin-managed static assets, and other non-reserved public paths can be registered by source plugins in code. If no core framework or plugin route matches a request, the request should return an unmatched result instead of falling back to the admin workspace index.html.

Workspace Entry Route Configuration​

The admin workspace entry is controlled by the core framework static configuration key workspace.basePath, which defaults to /admin. This configuration is evaluated during startup and determines which browser path serves the built-in workspace SPA static assets and refresh fallback:

workspace:
basePath: "/admin"

For a normal single-domain deployment, keeping /admin is recommended so the root path and other public paths remain available for portal pages, source-plugin-owned pages, or static assets. When the admin workspace uses a dedicated domain, workspace.basePath may also be set to /, allowing the whole domain to serve only the admin workspace.

Whichever entry path you use, it must not occupy a core framework-reserved namespace such as the core framework control plane /api, the unified plugin API namespace /x, the public plugin asset namespace /x-assets, or any other reserved path. After changing this configuration, keep the core framework public configuration, frontend route base, and deployment entry aligned.

How the Workspace Connects to Plugins​

Plugins connect their admin pages to the workspace navigation by declaring the menus field in plugin.yaml:

menus:
- key: plugin:linapro-demo-source:example
name: Example Management
path: linapro-demo-source-example
component: system/plugin/dynamic-page
perms: linapro-demo-source:example:view
type: M

When responding to frontend menu requests, the core framework merges built-in menus with menu declarations from enabled plugins, filters them by the current user's permissions, and returns the complete menu tree to the workspace. When a plugin is disabled, its menu entries automatically disappear from navigation, and the workspace does not need a code change.

This mechanism lets each plugin own and maintain its admin interface declaration. The core framework does not need to understand the plugin's business page content; it only aggregates menu data and filters permissions.

Workspace and Core Framework Responsibilities​

The workspace is the standard UI consumer of the core framework. It does not define business logic. The core framework provides API contracts, and the workspace uses those contracts to display data and expose operations. Plugins extend the framework's APIs, and the workspace hosts plugin pages through the unified menu and dynamic page shell mechanism without custom frontend changes for each plugin.

Developers can replace the built-in workspace with a custom frontend. As long as the new frontend follows the core framework's public RESTful API and permission model, it can use the same backend capabilities.

Core framework control-plane APIs, unified plugin APIs, and the default admin workspace are three separate boundaries:

BoundaryDefault PathDescription
Core framework control-plane API/api/v1/...Built-in core framework governance APIs for users, permissions, configuration, and related system capabilities
Unified plugin API/x/{plugin-id}/...Plugin API namespace; remaining path segments are organized by each plugin
Default admin workspacehttp://localhost:5666/adminDefault local development workspace address; not a core framework API route

Plugin Capability Boundaries​

Dynamic Routes​

The two plugin types differ clearly in route registration capability, but their plugin APIs must enter the same core framework-reserved namespace:

/x/{plugin-id}/...

Here, /x is the unified plugin API namespace, {plugin-id} isolates each plugin, and the remaining path segments are owned by the plugin. Official plugins are recommended to continue using /api/v1 as an internal version group, so common public paths look like /x/{plugin-id}/api/v1/.... The boundary enforced by the core framework, however, is /x/{plugin-id}, not a fixed /api/v1 child path.

Source plugins can register non-reserved HTTP route paths freely. A plugin declares a route registration callback during init(), and the core framework triggers it during startup. A plugin can register /, /portal/..., /assets/..., or other non-reserved public paths for pages, portals, static assets, custom fallback behavior, or other HTTP responses. This flexibility is useful, but it also creates a risk: if multiple source plugins register the same route path, route conflicts cause service startup to fail. For example, if more than one plugin registers the same GET / root route, the HTTP Server router reports a duplicate route and the process exits.

Plugin APIs for source plugins must not use arbitrary public paths. They must use the unified plugin API namespace. RouteRegistrar provides APIPrefix(), which returns the plugin namespace assigned to the current plugin:

/x/{plugin-id}
Tip

x stands for eXtension. It is the conventional namespace prefix that marks the path as a plugin extension API, not a core framework control-plane endpoint.

Source plugin business APIs should be mounted under that prefix. A plugin may register endpoints directly under the prefix, or add its own internal version group as needed. For example:

/x/linapro-demo-source/api/v1/demo-records
/x/linapro-demo-source/api/v1/demo-records/{id}

Source plugins must not register another plugin's path under /x. To keep boundaries clear, public pages, portal entries, static assets, health checks, and other non-API routes should not live under /x/{plugin-id}. Public pages and plugin-managed static assets should continue to use /, /portal/..., /assets/..., or another non-reserved path.

Dynamic plugins run inside a WASM sandbox and cannot directly access the HTTP Server route registration mechanism. Dynamic plugins declare their internal path, method, access, and permission through build-time RegisterRoutes metadata and the generated route contract. At runtime, the core framework owns only the /x/{plugin-id} prefix; the remaining path comes from the dynamic plugin's route contract. Typical public paths look like:

/x/linapro-demo-dynamic/api/v1/demo-records
/x/linapro-demo-dynamic/api/v1/demo-records/{id}
/x/linapro-demo-dynamic/api/v1/backend-summary

Source plugin requests are handled by the HTTP Server handlers registered by source plugins. Dynamic plugin requests are bridged by the core framework to the matching runtime based on the route contract. Core framework control-plane APIs still use /api/v1/...; neither source plugins nor dynamic plugins should mount plugin business APIs under the core framework control-plane namespace.

Plugin TypePath ConstraintConflict Risk
Source plugin APIMust use the /x/{plugin-id} namespace returned by APIPrefix(); remaining segments are plugin-defined, with /api/v1 recommended for internal versioningIsolated by plugin ID, so it does not conflict with another plugin's /x namespace
Source plugin custom routesCan register non-reserved paths such as /portal/... or /assets/...Startup fails if they conflict with the core framework or another plugin
Dynamic plugin APICore framework combines /x/{plugin-id} with the contract's internal path; remaining segments are defined by the plugin contract, with /api/v1 recommended for internal versioningAvoided by core framework isolation through plugin ID, active artifact, and route contract

Static Assets​

The core framework manages its own static assets through Go Embed and provides a declarative public_assets model for serving public plugin assets. Both source plugins and dynamic plugins can declare public asset directories in the root of plugin.yaml. The core framework maps those assets to /x-assets/{plugin-id}/{version}/..., for example:

/x-assets/linapro-demo-dynamic/v0.1.0/pages/standalone.html
/x-assets/linapro-demo-dynamic/v0.1.0/css/main.css

Each declaration uses source to point to a plugin-relative directory or a dynamic artifact frontend asset prefix, optional mount to specify its relative mount path under /x-assets/{plugin-id}/{version}/, and optional index to select the default file returned when the mount directory itself is requested. If mount is empty or /, files in the declared directory are mounted directly at the version root. If index is omitted, the core framework defaults to index.html.

public_assets:
# source points to the plugin directory that should be publicly exposed.
- source: frontend/public
# mount is the path under /x-assets/{plugin-id}/{version}/.
# / means the files are mounted directly at the version root.
mount: /
# index is the default file used when the mount directory itself is requested.
# When omitted, it defaults to index.html.
index: index.html
# Another asset group can be mounted under a subpath for pages, styles, or other groups.
- source: frontend/pages
mount: pages
index: standalone.html

When a source plugin registers an embedded filesystem through plugin.Assets().UseEmbeddedFiles(...), the core framework exposes files only from directories declared in public_assets. For example:

sourcemountExample file inside the pluginExample public URL
frontend/public/frontend/public/logo.png/x-assets/{plugin-id}/{version}/logo.png
frontend/pagespagesfrontend/pages/standalone.html/x-assets/{plugin-id}/{version}/pages/standalone.html

Files outside declared directories are not exposed simply because they exist in the embedded filesystem.

Dynamic plugin frontend assets must also use the same declarative model. The core framework no longer exposes dynamic artifact frontend assets automatically just because they exist. Only asset sets matching public_assets declarations are served through /x-assets/{plugin-id}/{version}/....

/x-assets is an optional hosted entry, not a mandatory frontend mechanism for source plugins. Source plugins can still return pages, file streams, static assets, or SPA fallback behavior through their own HTTP routes. Those routes are fully maintained by plugin code. The core framework does not infer HTTP routes from public_assets, and it does not infer asset directories from HTTP routes.

For dynamic plugins, workspace menus may reference assets hosted under /x-assets, but the menu path does not become the workspace route directly. The current embedded-mount mode usually uses component: system/plugin/dynamic-page, points the menu path at a .js or .mjs entry asset, and declares pluginAccessMode: embedded-mount in query. The core framework then forwards the hosted asset URL to the dynamic page shell as embeddedSrc.

menus:
- key: plugin:linapro-demo-dynamic:main-entry
name: Dynamic Plugin Demo
path: /x-assets/linapro-demo-dynamic/v0.1.0/mount.js
component: system/plugin/dynamic-page
perms: linapro-demo-dynamic:view
type: M
query:
pluginAccessMode: embedded-mount

Plugins can also expose static asset access through dynamic routes, but this is not recommended. The unified core framework asset entry provides governance capabilities that would otherwise need to be rebuilt:

Core Framework-Managed CapabilityProblem When Implemented Manually
Plugin enablement coupling, returning 404 automatically when disabledPlugin must implement enablement checks itself
Automatic MIME type derivationPlugin must maintain its own Content-Type map
In-memory caching and on-demand servingPlugin must implement caching itself
Version path isolation through /{version}/...Plugin must design its own versioning strategy
Consistent priority rules with core framework asset routesCustom implementation may conflict with core framework route ordering

public_assets is an explicit publication boundary set by the plugin author. As long as the declaration points to a real directory in the plugin-owned resource set or a dynamic artifact frontend asset prefix, the core framework treats it as ordinary public static content. For that reason, plugin authors must declare only files that are safe for anonymous access. Do not put governance metadata, installation scripts, private configuration, tenant-specific files, or user-specific files into public_assets.

The core framework still applies strict path validation. The following declarations are rejected:

Invalid ConfigurationReason
Empty sourceNo clear publication boundary can be formed
Absolute paths or URLsThey may escape the plugin-owned resource set
Paths that normalize to ../ traversalThey may escape the declared directory or plugin root
Paths containing wildcards, query strings, or fragmentsThey cannot be mapped to a stable static directory
Duplicate or overlapping mount valuesA single public URL could map to multiple sources
Missing source directory or prefixSource plugin directories and dynamic frontend asset prefixes must exist
Symlinks that escape the plugin rootThey may read files outside the plugin
index values that are not plain file namesDirectory defaults must be safe relative file names

A public asset URL uses {plugin-id, version} as the cache boundary. Asset content under the same plugin version must remain stable. If asset content changes, the plugin should upgrade its plugin.yaml version or introduce an equivalent content versioning mechanism. When a plugin is not installed, not enabled, or not available to the current tenant, /x-assets/{plugin-id}/{version}/... returns 404 by default. Dynamic plugins may continue serving installed or active versioned assets while the plugin remains enabled; source plugins resolve declared resources from the plugin assets compiled into the core framework or from the plugin directory.

Foundational Capabilities​

The core framework exposes foundational capabilities to plugins through stable service adapter interfaces. Plugins can call these interfaces without knowing the core framework's internal implementation.

Source plugins access core framework foundational capabilities through the HostServices interface, provided consistently by pluginhost.HTTPRegistrar and pluginhost.CronRegistrar:

func registerRoutes(ctx context.Context, registrar pluginhost.HTTPRegistrar) error {
svc := registrar.HostServices()
// Use core framework-provided i18n, tenant filtering, cache, and related capabilities.
_ = svc.I18n()
_ = svc.TenantFilter()
_ = svc.Cache()
_ = svc.Config()
_ = svc.HostConfig()
_ = svc.Manifest()
_ = svc.Notify()
// ...
return nil
}

Current HostServices covers the following service domains:

ServiceCapability
APIDoc()API documentation localization adapter
Auth()Tenant authentication adapter
BizCtx()Business context adapter
Cache()Plugin-scoped cache adapter
Config()Reads the current plugin's own static configuration; it is scoped to the plugin and does not fall back to scanning the full host configuration tree
HostConfig()Reads allowlisted public host configuration keys such as workspace.basePath, i18n.default, and i18n.enabled
I18n()Runtime translation adapter
Manifest()Reads declaration resources under the current plugin's manifest/, such as metadata.yaml, excluding dedicated config, sql, and i18n resource directories
Notify()Notification sender adapter
PluginState()Plugin enablement state adapter
PluginLifecycle()Plugin lifecycle orchestration adapter
Route()Dynamic route metadata adapter
Session()Online session adapter
TenantFilter()Tenant filter adapter

Dynamic plugins access core framework capabilities through the host_call mechanism exposed by pluginbridge. Every call is validated against the authorization snapshot. A dynamic plugin declares hostServices in plugin.yaml to request the services and methods it needs. The core framework records that authorization snapshot at installation time, then validates the service, method, and resource boundary for every runtime host_call:

hostServices:
- service: data
methods: [list, get, create, update, delete]
resources:
tables:
- plugin_demo_dynamic_record
- service: cache
methods: [get, set, delete]
- service: runtime
methods: [log.write, info.uuid, info.now]
- service: config
methods: [get]
- service: hostConfig
methods: [get]
resources:
keys:
- workspace.basePath
- i18n.default
- service: manifest
methods: [get]
resources:
paths:
- metadata.yaml

Current core framework service capabilities available to dynamic plugins include:

ServiceCapability
runtimeRuntime logs, plugin-scoped runtime state read/write/delete, core framework time, UUID, node information, and other runtime metadata
cronDynamic plugin scheduled task registration
storageFile write, read, delete, list, and metadata operations constrained by authorized paths
networkOutbound HTTP requests constrained by authorized URL patterns
dataList, detail, create, update, delete, and transaction operations constrained by authorized data tables
cachePlugin cache read, write, delete, increment, and expiry adjustment
lockDistributed lock acquire, renew, and release
notifyMessage notification sending
configRead-only access to the current plugin's own configuration; dynamic plugin manifests declare only methods: [get], while typed reads are SDK convenience methods
hostConfigRead-only access to allowlisted public host configuration keys; readable keys must be declared through resources.keys
manifestRead-only access to plugin manifest/ declaration resources; readable paths must be declared through resources.paths

For storage, accessible paths are constrained by paths; for data, accessible tables are constrained by tables; for network, accessible targets are constrained by declared URL resource patterns; for hostConfig, host keys are constrained by keys; and for manifest, declaration resources are constrained by paths. Other services continue to be controlled by methods.

The core framework's foundational capabilities will continue to expand across versions. Plugins can use only the capabilities they actually need without depending on core framework internal service implementations.

Portal Routes and Admin Routes​

When implementing business capabilities, plugins often need two kinds of interfaces: portal routes for frontend users (the data plane), and admin routes for backend management (the control plane). Their callers, permission models, and interface semantics differ significantly.

DimensionPortal Routes (Data Plane)Admin Routes (Control Plane)
CallerFrontend users and anonymous visitorsAdministrators and backend operators
AuthenticationCan be public or require login as neededUsually requires login and permissions
Interface semanticsContent viewing and business operationsData management and configuration changes
Menu mountingNot necessarily mounted in the workspaceUsually accessed through workspace menus

The core framework does not interpret a plugin's internal route grouping and does not distinguish the plugin's control plane from its data plane. Plugins own this internal grouping entirely. Note that /x/{plugin-id} carries the plugin API namespace only. Portal APIs and admin APIs can live directly under plugin-defined subpaths such as /x/{plugin-id}/portal/... and /x/{plugin-id}/admin/.... If the plugin adds a version group, it is only another plugin-owned path segment, not a core framework-mandated prefix. Public pages, portal entries, static assets, or custom fallback routes for source plugins are not plugin APIs and should continue to use non-reserved paths such as /portal/* or /assets/*. The following example shows a source plugin registering a public portal entry, portal APIs, and admin APIs together:

func registerRoutes(ctx context.Context, registrar pluginhost.HTTPRegistrar) error {
var (
routes = registrar.Routes()
middlewares = routes.Middlewares()
apiPrefix = routes.APIPrefix()
)
// Plugin APIs: both source plugins and dynamic plugins must enter the
// /x/{plugin-id} namespace. Here, /api/v1 is the plugin's internal API version group.
routes.Group(apiPrefix, func(group pluginhost.RouteGroup) {
group.Middleware(
middlewares.NeverDoneCtx(),
middlewares.HandlerResponse(),
middlewares.CORS(),
middlewares.RequestBodyLimit(),
middlewares.Ctx(),
)

// Portal API (data plane): for frontend users; some endpoints can be anonymous.
// Full path example: /x/my-plugin/api/v1/portal/articles.
group.Group("/portal", func(group pluginhost.RouteGroup) {
// Public endpoint: no login required.
group.Bind(
portalArticleCtrl,
)
// Login endpoint: requires authentication but not admin permission.
group.Group("/", func(group pluginhost.RouteGroup) {
group.Middleware(
middlewares.Auth(),
)
group.POST("/comments", commentCtrl.Create)
})
})

// Admin API (control plane): for the admin workspace; requires full auth and permission checks.
// Full path example: /x/my-plugin/api/v1/admin/articles.
group.Group("/admin", func(group pluginhost.RouteGroup) {
group.Middleware(
middlewares.Auth(),
middlewares.Tenancy(),
middlewares.Permission(),
)
group.Bind(
adminArticleCtrl,
adminCommentCtrl,
)
})
})

// Public portal entry: plugin-managed pages, static assets, or SPA fallback.
// These routes are not plugin APIs, do not enter /x, and are not automatically projected as menus, permissions, or OpenAPI entries.
routes.Group("/portal", func(group pluginhost.RouteGroup) {
group.Middleware(
middlewares.NeverDoneCtx(),
middlewares.CORS(),
middlewares.RequestBodyLimit(),
)
group.Bind(
portalPageCtrl,
portalAssetCtrl,
)
})

return nil
}

In this example, /portal is a plugin-managed public portal entry, /x/my-plugin/api/v1/portal/articles is a plugin portal API, and /x/my-plugin/api/v1/admin/articles is a plugin admin API. The admin workspace sees plugin menu entries only through menus in plugin.yaml; it must not automatically generate workspace routes, menus, or permission nodes just because a plugin registered HTTP routes. Permission identifiers for admin APIs are declared through the permission field in g.Meta, then associated with menu entries and permission items in plugin.yaml, and finally shown to administrators through the workspace extension center:

// Example admin endpoint DTO.
type ArticleAdminListReq struct {
g.Meta `path:"/articles" method:"get" tags:"My Plugin Admin" summary:"List admin articles" permission:"my-plugin:article:view"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
}

Why These Boundaries Matter​

The responsibility boundary design between the core framework and each component is not only about today's feature division. It also shapes how the whole ecosystem evolves over time.

  • For the core framework, stable extension interfaces mean internal implementation can evolve safely. The core framework can refactor concrete HostServices implementations, optimize plugin governance flows, and add new foundational capabilities. As long as the public pluginhost and pluginbridge contracts remain stable, existing plugins are not affected.

  • For plugin developers, clear boundaries mean core framework capabilities can be used with confidence. Developers do not need to understand core framework internals or worry that future core framework upgrades will break plugin logic. If a plugin collaborates with the core framework strictly through stable contracts, version compatibility is protected by the core framework's interface stability.

  • For the system as a whole, this layered architecture allows different teams to maintain the core framework and their own plugins independently, reducing coordination friction and leaving enough room for future plugin types or governance capabilities.