Skip to main content
Version: 0.2.x

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 PrefixPurpose
/api/v1Current 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
/adminDefault 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:

MiddlewareResponsibility
ghttp.MiddlewareNeverDoneCtxReplaces the request Context with a non-cancellable copy, preventing client disconnection from prematurely terminating business logic
middlewareSvc.ResponseSerializes a unified JSON response envelope, localizes business error messages, and transparently passes through 304, 204, and streaming responses
middlewareSvc.CORSCalls CORSDefault, allowing cross-origin requests and handling OPTIONS preflight
middlewareSvc.RequestBodyLimitCaps non-multipart bodies at 100 MB; for multipart uploads, the limit is computed dynamically from the sys.upload.maxSize configuration
middlewareSvc.CtxInjects 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:

MiddlewareResponsibility
middlewareSvc.AuthParses the JWT from Authorization: Bearer <token>, validates the signature and session validity, and writes the user identity into the request context
middlewareSvc.TenancyResolves the tenant identity from the request context and injects the tenant ID; defaults to the platform tenant when multi-tenancy is disabled
middlewareSvc.PermissionReads 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:

  1. Reads the Bearer Token from the Authorization request header
  2. Parses the JWT and validates the signature and expiry
  3. Calls SessionStore.TouchOrValidate to refresh session activity and verify the session still exists (supports forced logout and timeout cleanup)
  4. Writes the user identity (user ID, tenant ID, token ID, etc.) into the request context

The Permission middleware authorization flow:

  1. Reads the permission field from g.Meta tags; supports multiple permissions separated by commas
  2. Loads the current user's access context (permission list, data scope)
  3. 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 FieldScopeDescription
pathCore framework / Source plugin / Dynamic pluginEndpoint route path
methodCore framework / Source plugin / Dynamic pluginHTTP method, e.g. get, post
tagsCore framework / Source plugin / Dynamic pluginGrouping tags used for OpenAPI document categories
summaryCore framework / Source plugin / Dynamic pluginShort endpoint description, shown in docs and plugin management UI
dcCore framework / Source pluginDetailed endpoint description (description shorthand)
permissionCore framework / Source plugin / Dynamic pluginPermission identifier enforced by the Permission middleware
mimeCore framework / Source pluginRequest body MIME type, e.g. multipart/form-data
accessDynamic pluginAccess control — public for anonymous, login for authenticated
operLogDynamic pluginOperation 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 /admin is 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 Auth must compose middleware as Auth → 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:

FieldValueDescription
accesspublicAnonymous access — no authentication required
accessloginRequires an authenticated session — the core framework validates the JWT
permissione.g. linapro-demo-dynamic:record:createRequires a specific permission — the core framework checks it against the user's permission set

Source Plugin vs. Dynamic Plugin Routing Comparison​

DimensionSource PluginDynamic Plugin
Registration methodCallback registered via HTTPRegistrar at startupRoute contracts parsed from WASM artifact at load time
Path restrictionPlugin APIs must use /x/{plugin-id}; non-reserved public paths can also be registeredForced under /x/{plugin-id}/
Middleware compositionPlugin selects and combines from RouteMiddlewaresManaged entirely by the core framework; plugin influences auth behavior via access field
Permission declarationInline in DTO's g.Meta tagpermission field in RouteContract
OpenAPI documentAutomatically aggregated into core framework docsCore framework reads and aggregates from route contracts
Route conflict riskDeveloper responsibilityAvoided 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.