Introductionâ
Source plugins are LinaPro's recommended default extension method. They are compiled and deployed together with the core framework as Go source code, using pluginhost to register routes, hooks, scheduled tasks, lifecycle callbacks, and governance logic. They are suited for long-term business modules that require high performance and a complete engineering experience.
Official source plugins live in apps/lina-plugins/. The main repository mounts the official plugin workspace through this directory. User projects also maintain their own business plugins here.
When to Useâ
| Scenario | Recommended? | Reason |
|---|---|---|
| Long-term business modules | Yes | Testable, auditable, best performance |
| Organization, content, monitoring capabilities | Yes | Tight integration with core framework permissions, menus, scheduling, and multi-tenancy |
| Runtime hot-loading | Not preferred | Source plugins require rebuilding and redeploying the core framework |
| Commercial binary distribution | Not preferred | Source plugins typically expose source code |
Standard Directory Structureâ
apps/lina-plugins/<plugin-id>/
âââ plugin.yaml
âââ plugin_embed.go
âââ backend/
â âââ api/ # API DTOs and route contracts
â âââ internal/
â â âââ controller/ # HTTP controllers
â â âââ service/ # Business service layer
â â âââ dao/ # gf gen dao generated
â â âââ model/ # do/entity models
â âââ plugin.go # Plugin registration entry
âââ frontend/
â âââ pages/ # Plugin pages
âââ manifest/
â âââ sql/ # Installation and upgrade SQL
â â âââ mock-data/ # Demo data, optional
â â âââ uninstall/ # Uninstall SQL
â âââ i18n/ # Plugin language packs
âââ README.md
backend/internal/service/ is the fixed location for plugin service logic. Do not create a service/ package at the plugin root or backend/ root.
Plugin Manifestâ
plugin.yaml declares plugin identity, runtime form, multi-tenant boundaries, menus, and permissions:
id: content-article
name: æįĢ įŽĄį
version: v0.1.0
type: source
scope_nature: tenant_aware
supports_multi_tenant: true
default_install_mode: tenant_scoped
description: æäžæįĢ å
厚įåĸå æšæĨįŽĄįåčŊ
author: linapro
license: Apache-2.0
menus:
- key: plugin:content-article:list
name: æįĢ įŽĄį
path: content-article-list
component: system/plugin/dynamic-page
perms: content-article:article:view
icon: ant-design:file-text-outlined
type: M
sort: 1
- key: plugin:content-article:create
parent_key: plugin:content-article:list
name: ååģēæįĢ
perms: content-article:article:create
type: B
Menu key values must be globally unique. Use the plugin:<plugin-id>:<menu-key> format. Button permissions are attached under menus via type: B and do not appear in the sidebar directly.
Database and SQLâ
Installation SQL is located in manifest/sql/, and uninstall SQL in manifest/sql/uninstall/. Installation and upgrade scripts must be idempotent â use CREATE TABLE IF NOT EXISTS, CREATE INDEX IF NOT EXISTS, and similar patterns.
CREATE TABLE IF NOT EXISTS content_article_record (
"id" BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
"tenant_id" INT NOT NULL DEFAULT 0,
"title" VARCHAR(255) NOT NULL DEFAULT '',
"content" TEXT NOT NULL DEFAULT '',
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_content_article_record_tenant
ON content_article_record ("tenant_id");
Plugin tables that need multi-tenant support should include a tenant_id column. When the multi-tenant plugin is not enabled, tenant_id = 0 represents the platform context.
API and Service Layerâ
Plugin API definitions also use g.Meta to declare paths, methods, permissions, and documentation:
type ArticleListReq struct {
g.Meta `path:"/plugins/content-article/article" method:"get" tags:"Article" summary:"List articles" permission:"content-article:article:view"`
Page int `json:"page" v:"min:1"`
PageSize int `json:"pageSize" v:"min:1,max:100"`
}
The service layer accesses the database through the plugin's own DAO. When tenant isolation is needed, use the core framework-published TenantFilterService to append tenant conditions rather than hand-writing inconsistent filter rules.
Registration Entryâ
Source plugins register in backend/plugin.go through init():
func init() {
plugin := pluginhost.NewSourcePlugin("content-article")
plugin.Assets().UseEmbeddedFiles(contentarticle.EmbeddedFiles)
plugin.HTTP().RegisterRoutes(
pluginhost.ExtensionPointHTTPRouteRegister,
pluginhost.CallbackExecutionModeBlocking,
registerRoutes,
)
plugin.Cron().RegisterCron(
pluginhost.ExtensionPointCronRegister,
pluginhost.CallbackExecutionModeBlocking,
registerCronJobs,
)
plugin.Lifecycle().RegisterBeforeUpgradeHandler(beforeUpgrade)
plugin.Lifecycle().RegisterAfterUpgradeHandler(afterUpgrade)
pluginhost.RegisterSourcePlugin(plugin)
}
The core framework generates an aggregation entry in plugin-complete mode, blank-importing configured plugins so their init() registration logic enters the core framework process.
Frontend Pagesâ
Source plugin frontend pages live in frontend/pages/ and are loaded by the core framework workspace's dynamic page shell. The component field in plugin menus typically uses:
component: system/plugin/dynamic-page
Pages can reuse the default workspace's frontend ecosystem and design conventions. When a plugin is disabled, the core framework menu API no longer returns that plugin's entry, and the workspace sidebar hides it automatically.
Event Hooks and Scheduled Tasksâ
Source plugins can subscribe to core framework events â for example, login success, plugin enablement, and system startup. Hooks can be synchronous-blocking or asynchronous, depending on the execution mode chosen at registration.
Plugins can also register their own scheduled task handlers for administrators to select when creating tasks in the admin workspace:
plugin.Cron().RegisterCron(
pluginhost.ExtensionPointCronRegister,
pluginhost.CallbackExecutionModeBlocking,
func(registry pluginhost.CronRegistry) error {
registry.Register("content-article:cleanup", cleanupExpiredArticles)
return nil
},
)
Runtime Upgradeâ
After source plugin files are updated, the core framework compares the active version in the database with the currently discovered version. When a higher version is found, the plugin enters the pending_upgrade runtime state. Core framework base governance capabilities remain available, while plugin business entry points enter a controlled state.
The administrator executes an explicit runtime upgrade in the plugin management page. The upgrade flow re-reads the active and target manifests, runs dependency checks, BeforeUpgrade callbacks, plugin custom upgrade logic, upgrade SQL, governance resource synchronization, active version switching, and cache invalidation. On failure, it enters upgrade_failed â diagnostics can be reviewed and the upgrade retried.
This model prevents file overwrites from being mistaken for completed data and governance resource upgrades.
Best Practicesâ
- Use
kebab-casefor pluginIDand the correspondingsnake_casefor database table prefixes. - Installation and upgrade
SQLmust be idempotent â avoid failures when reinstalling with retained data. - Place service logic in
backend/internal/service/. - Use only stable contracts like
pluginhostandpluginserviceâ do not depend on the core framework'sinternal/packages directly. - Reserve a
tenant_idcolumn in multi-tenant plugin tables and use the core framework tenant filter service. - Declare menus and button permissions together to avoid pages being visible but operations lacking permission.
- Distinguish governance records, database data, and file data during uninstallation to avoid accidental deletion.