Overviewâ
The routing system of lina-core is built on top of GoFrame's ghttp.Server, organized around four dimensions: version prefixes, middleware chains, permission declarations, and plugin extension interfaces. The core framework manages its own API versions under the /api/v1 prefix, enforces CORS, authentication, and permission governance through an ordered middleware chain, maintains all API attributes inline with code via g.Meta struct tags, and provides differentiated routing strategies for the two plugin types.
API Version Managementâ
All core framework control-plane APIs are mounted under the /api/v1 router group. The version prefix is declared via server.Group. When a future /api/v2 is needed, only a new group needs to be added â existing v1 endpoints remain unaffected.
server.Group("/api/v1", func(group *ghttp.RouterGroup) {
bindHostAPIMiddlewares(group, middlewareSvc)
bindPublicStaticAPIRoutes(group, ...)
bindProtectedStaticAPIRoutes(group, middlewareSvc, ...)
})
| Path Prefix | Purpose |
|---|---|
/api/v1 | Current stable core framework API version â authentication, users, roles, plugin management, and other control-plane endpoints |
/x/{plugin-id}/... | Shared plugin API namespace for source plugins and dynamic plugins; APIPrefix() returns /x/{plugin-id}, and the remaining path is organized by the plugin |
/x-assets/{plugin-id}/{version} | Public plugin static asset hosting entry |
/admin | Default admin workspace frontend route base; locally accessed through http://localhost:5666/admin, not a core framework control-plane API |
/ | No longer falls back to the admin workspace by default; usually reserved for portals, custom pages, or source-plugin-managed routes |
The guiding principle for version management is: a router group is a version boundary. Different API versions coexist in the same process, each with its own middleware configuration and handler set â no Content-Type negotiation or special request headers are used for versioning.
Middleware Pipelineâ
The core framework divides middleware into two categories: request-chain middleware mounted on router groups, and global middleware registered at the server level. Request-chain middleware executes in declaration order; if any middleware calls r.ExitAll(), the chain stops immediately.
Common Base Middlewareâ
The following middleware applies to both the /api/v1 group and the plugin API group /x, forming the base processing pipeline for every request:
| Middleware | Responsibility |
|---|---|
ghttp.MiddlewareNeverDoneCtx | Replaces the request Context with a non-cancellable copy, preventing client disconnection from prematurely terminating business logic |
middlewareSvc.Response | Serializes a unified JSON response envelope, localizes business error messages, and transparently passes through 304, 204, and streaming responses |
middlewareSvc.CORS | Calls CORSDefault, allowing cross-origin requests and handling OPTIONS preflight |
middlewareSvc.RequestBodyLimit | Caps non-multipart bodies at 100 MB; for multipart uploads, the limit is computed dynamically from the sys.upload.maxSize configuration |
middlewareSvc.Ctx | Injects the business context (user identity placeholder, tenant placeholder, request locale), and sets the Content-Language response header |
Authentication and Permission Middlewareâ
The following middleware is mounted only on protected route sub-groups. Public endpoints such as login and health probes do not pass through this layer:
| Middleware | Responsibility |
|---|---|
middlewareSvc.Auth | Parses the JWT from Authorization: Bearer <token>, validates the signature and session validity, and writes the user identity into the request context |
middlewareSvc.Tenancy | Resolves the tenant identity from the request context and injects the tenant ID; defaults to the platform tenant when multi-tenancy is disabled |
middlewareSvc.Permission | Reads the permission field from the DTO's g.Meta tag or a manual declaration, then checks whether the current user holds the required permission |
The middleware execution order is shown below:
Middleware Available to Source Pluginsâ
The core framework publishes the above middleware to source plugins through the RouteMiddlewares interface. Plugins compose the middleware they need without directly depending on internal core framework packages:
routes.Group(routes.APIPrefix(), func(group pluginhost.RouteGroup) {
group.Middleware(
middlewares.NeverDoneCtx(),
middlewares.HandlerResponse(),
middlewares.CORS(),
middlewares.RequestBodyLimit(),
middlewares.Ctx(),
)
// Public sub-group
group.Group("/", func(group pluginhost.RouteGroup) {
group.Bind(demoController.Ping)
})
// Protected sub-group
group.Group("/", func(group pluginhost.RouteGroup) {
group.Middleware(
middlewares.Auth(),
middlewares.Tenancy(),
middlewares.Permission(),
)
group.Bind(demoController.ListRecords, ...)
})
})
Auth Route Designâ
The core framework separates routes into public routes and protected routes using middleware differences between router sub-groups, rather than relying on path conventions or special markers.
Route Layeringâ
Permission Declarationâ
The permission identifier for a protected endpoint is declared inline in the DTO's g.Meta tag. The Permission middleware reads this identifier at runtime and checks it against the current user's role permissions:
type UserListReq struct {
g.Meta `path:"/users" method:"get" tags:"User" summary:"List users" permission:"user:list"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
}
The Auth middleware authentication flow:
- Reads the
Bearer Tokenfrom theAuthorizationrequest header - Parses the JWT and validates the signature and expiry
- Calls
SessionStore.TouchOrValidateto refresh session activity and verify the session still exists (supports forced logout and timeout cleanup) - Writes the user identity (user ID, tenant ID, token ID, etc.) into the request context
The Permission middleware authorization flow:
- Reads the
permissionfield fromg.Metatags; supports multiple permissions separated by commas - Loads the current user's access context (permission list, data scope)
- Matches the required permissions using OR semantics â any one satisfied grants access; the wildcard
*:*:*bypasses the check for super administrators
For a detailed treatment of authentication (JWT issuance, session management, RBAC permission model), see the permission management chapter.
Inline API Attribute Managementâ
lina-core uses the g.Meta mechanism to declare all API attributes â path, method, grouping tags, summary, description, permission, MIME type, and more â inline in the request DTO's struct tags, achieving code and documentation from a single source of truth.
Core Framework Endpoint Tag Exampleâ
type CreateRecordReq struct {
g.Meta `path:"/records" method:"post" mime:"multipart/form-data" tags:"Source Plugin Demo" summary:"Create source plugin sample record" dc:"Create a sample record with an optional attachment." permission:"linapro-demo-source:example:create"`
Title string `json:"title" v:"required|length:1,128" dc:"Record title"`
Content string `json:"content" dc:"Record content"`
}
In a source plugin DTO, path is relative to the plugin route group where the controller is bound. The final public path is the combination of the source plugin's routes.APIPrefix() and the DTO path, for example /x/linapro-demo-source/records.
Dynamic Plugin Endpoint Tag Exampleâ
Dynamic plugins use gmeta.Meta instead of g.Meta because of component dependencies in the sandboxed environment, with additional access and operLog fields:
type CreateDemoRecordReq struct {
gmeta.Meta `path:"/demo-records" method:"post" tags:"Dynamic Plugin Demo" summary:"Create dynamic plugin sample record" access:"login" permission:"linapro-demo-dynamic:record:create" operLog:"create"`
Title string `json:"title" v:"required|length:1,128"`
Content string `json:"content"`
}
Common Tag Fieldsâ
| Tag Field | Scope | Description |
|---|---|---|
path | Core framework / Source plugin / Dynamic plugin | Endpoint route path |
method | Core framework / Source plugin / Dynamic plugin | HTTP method, e.g. get, post |
tags | Core framework / Source plugin / Dynamic plugin | Grouping tags used for OpenAPI document categories |
summary | Core framework / Source plugin / Dynamic plugin | Short endpoint description, shown in docs and plugin management UI |
dc | Core framework / Source plugin | Detailed endpoint description (description shorthand) |
permission | Core framework / Source plugin / Dynamic plugin | Permission identifier enforced by the Permission middleware |
mime | Core framework / Source plugin | Request body MIME type, e.g. multipart/form-data |
access | Dynamic plugin | Access control â public for anonymous, login for authenticated |
operLog | Dynamic plugin | Operation log type â create, update, delete, other |
This approach consolidates endpoint definition, documentation metadata, and permission declarations in the same DTO file. The core framework automatically aggregates the OpenAPI document from these tags, eliminating the need to maintain separate annotation files or documentation, and fundamentally removing the risk of drift between code and docs.
Source Plugin Routingâ
Source plugins are compiled and delivered with the core framework binary and register routes through Routes() from pluginhost.HTTPRegistrar. Source plugin APIs must enter their own /x/{plugin-id} namespace. Source plugins can also register non-reserved public routes for portal pages, public assets, custom fallbacks, and other HTTP response logic.
Registrationâ
Source plugins declare a route-registration callback in their init() function. The core framework triggers all callbacks during the registerSourcePluginHTTPRoutes startup phase:
plugin.HTTP().RegisterRoutes(
pluginhost.ExtensionPointHTTPRouteRegister,
pluginhost.CallbackExecutionModeBlocking,
registerRoutes,
)
The registerRoutes callback receives an HTTPRegistrar and creates route groups via Routes().Group(), composing core framework-published middleware through Middlewares():
func registerRoutes(ctx context.Context, registrar pluginhost.HTTPRegistrar) error {
routes := registrar.Routes()
middlewares := routes.Middlewares()
routes.Group(routes.APIPrefix(), func(group pluginhost.RouteGroup) {
// Base middleware
group.Middleware(
middlewares.NeverDoneCtx(),
middlewares.HandlerResponse(),
middlewares.CORS(),
middlewares.RequestBodyLimit(),
middlewares.Ctx(),
)
// Public sub-group
group.Group("/", func(group pluginhost.RouteGroup) {
group.Bind(demoController.Ping)
})
// Protected sub-group (must follow Auth -> Tenancy -> Permission order)
group.Group("/", func(group pluginhost.RouteGroup) {
group.Middleware(
middlewares.Auth(),
middlewares.Tenancy(),
middlewares.Permission(),
)
group.Bind(demoController.CreateRecord, demoController.ListRecords, ...)
})
})
return nil
}
Plugin API Namespace and Custom Routesâ
RouteRegistrar.APIPrefix() returns the mandatory API namespace for the current plugin:
/x/{plugin-id}
Source plugin business APIs must live under this namespace. The remaining path is plugin-defined, for example:
/x/linapro-demo-source/records
/x/linapro-demo-source/records/{id}
Source plugins must not register another plugin's path under /x. The core framework rejects source plugin route registrations that cross the plugin's own /x/{plugin-id} boundary.
Source plugins can register non-reserved paths such as /, /portal/..., and /assets/.... This freedom comes with the following expectations:
- Avoid conflicting with core framework routes: The core framework occupies
/api,/x, and/x-assets; source plugin public pages should use clear non-reserved namespaces, and/adminis normally reserved for the default workspace frontend route - Avoid conflicts between plugins: When multiple source plugins are installed, collisions on non-reserved paths cause route registration to fail and stop program startup â developers must ensure path uniqueness
- Protected routes must follow the correct middleware order: Any sub-group that uses
Authmust compose middleware asAuth â Tenancy â Permission; the core framework enforces this constraint through automated tests
Route Captureâ
During the registration phase the core framework captures all SourceRouteBinding records from source plugins and aggregates documentable endpoints into the core framework OpenAPI document automatically â no additional developer action is required.
Dynamic Plugin Routingâ
Dynamic plugins (WASM plugins) have their routes fully managed by the core framework. The plugin itself does not interact with GoFrame's route registration mechanism directly, and routing capabilities are explicitly constrained.
Namespace Constraintâ
All dynamic plugin API routes are forced under the /x/{plugin-id}/ prefix. The remaining path comes from the dynamic plugin's own route contract, for example:
/x/linapro-demo-dynamic/backend-summary
/x/linapro-demo-dynamic/demo-records
/x/linapro-demo-dynamic/demo-records/{id}
This constraint is enforced by a wildcard catch-all handler bound to the /x router group. A plugin cannot bind to the core framework /api/v1 control plane or any API path outside /x, ensuring dynamic plugins cannot disrupt the core framework routing structure.
Route Declarationâ
Dynamic plugin routes are declared through RouteContract embedded in the WASM artifact â not registered at runtime. The core framework parses route contracts when loading the artifact; incoming requests are matched by the core framework-side PrepareDynamicRouteMiddleware:
// Route contract embedded in the WASM artifact
type RouteContract struct {
Path string // Plugin-internal path, e.g. /demo-records
Method string // HTTP method
Tags []string // Grouping tags
Summary string // Short description
Access string // "public" or "login"
Permission string // Permission identifier
Meta map[string]string // Plugin-defined metadata
RequestType string // Request type name used for reflective dispatch
}
The Path field is the plugin-internal path. The core framework prepends /x/{plugin-id} when exposing it externally. If the internal path is /demo-records, the final public path is /x/{plugin-id}/demo-records.
Dynamic Route Request Flowâ
Dynamic Plugin Permission Declarationâ
Dynamic plugin route access is declared through the access and permission fields of RouteContract:
| Field | Value | Description |
|---|---|---|
access | public | Anonymous access â no authentication required |
access | login | Requires an authenticated session â the core framework validates the JWT |
permission | e.g. linapro-demo-dynamic:record:create | Requires a specific permission â the core framework checks it against the user's permission set |
Source Plugin vs. Dynamic Plugin Routing Comparisonâ
| Dimension | Source Plugin | Dynamic Plugin |
|---|---|---|
| Registration method | Callback registered via HTTPRegistrar at startup | Route contracts parsed from WASM artifact at load time |
| Path restriction | Plugin APIs must use /x/{plugin-id}; non-reserved public paths can also be registered | Forced under /x/{plugin-id}/ |
| Middleware composition | Plugin selects and combines from RouteMiddlewares | Managed entirely by the core framework; plugin influences auth behavior via access field |
| Permission declaration | Inline in DTO's g.Meta tag | permission field in RouteContract |
| OpenAPI document | Automatically aggregated into core framework docs | Core framework reads and aggregates from route contracts |
| Route conflict risk | Developer responsibility | Avoided by core framework namespace constraints |
Global Middleware Extensionâ
In addition to group-level middleware, source plugins can register server-level global middleware via GlobalMiddlewares(), applying to all requests matching a specified pattern:
err := registrar.GlobalMiddlewares().Bind(
pluginhost.MiddlewareScope("/*"),
func(r *ghttp.Request) {
// Plugin-specific global request interception logic
// (only executes when the plugin is enabled)
r.Middleware.Next()
},
)
The core framework automatically injects a plugin-enabled state guard into global middleware. When the plugin is disabled, the middleware logic is skipped transparently â developers do not need to handle plugin state checks themselves.