Plugin System Guide
This guide explains how to extend Business M by creating and integrating new domain plugins.
1. Backend Plugin Structure
Section titled “1. Backend Plugin Structure”A backend plugin is a standard Framework M App defined as a library.
Folder Structure
Section titled “Folder Structure”libs/my_plugin/├── src/│ └── my_plugin/│ ├── __init__.py # App definition│ ├── doctypes/ # Domain entities│ ├── services/ # Business logic│ └── bootstrap.py # DI wiring & extensions├── pyproject.toml└── README.mdEntry Point
Section titled “Entry Point”In pyproject.toml, register the app and optional bootstrap/container:
[project.entry-points."framework_m.apps"]my_plugin = "my_plugin:app"
[project.entry-points."framework_m.bootstrap"]my_plugin_init = "my_plugin.bootstrap:MyPluginBootstrap"2. Extending Core DocTypes
Section titled “2. Extending Core DocTypes”Use the Bounded Context pattern. Instead of adding fields directly to core Item, create a MyPluginItem in your library:
class MyPluginItem(BaseDocType): item: str = Field(description="Link to core Item") special_field: str = Field(...)
class Meta: table_name = "my_plugin_item"3. Frontend Plugin Structure
Section titled “3. Frontend Plugin Structure”Frontend plugins are integrated into the Desk UI at build-time.
Folder Structure
Section titled “Folder Structure”libs/my_plugin/frontend/├── src/│ ├── pages/│ ├── components/│ └── plugin.config.ts # Plugin Manifest├── package.json└── vite.config.tsManifest (plugin.config.ts)
Section titled “Manifest (plugin.config.ts)”The manifest defines how your plugin integrates into the UI:
import type { FrameworkMPlugin } from "@framework-m/plugin-sdk";
const plugin: FrameworkMPlugin = { name: "my_plugin", version: "0.1.0",
manifests: [ { app_id: "my_plugin", label: "My Plugin", icon: "box", resources: [ { name: "my_plugin.dashboard", label: "Dashboard", route: "/my-plugin/dashboard", } ] } ],
routes: [ { path: "/my-plugin/dashboard", element: () => import("./pages/Dashboard"), } ]};
export default plugin;4. Integration into Business M
Section titled “4. Integration into Business M”To enable your plugin in the main application:
Backend Integration
Section titled “Backend Integration”Add the library to apps/business-m/pyproject.toml or install it in the environment.
cd apps/business-muv add --path ../../libs/my_pluginFrontend Integration
Section titled “Frontend Integration”Add the plugin path to apps/business-m/frontend/plugin.config.ts (or the discovery configuration).
Ensure the library is linked via pnpm:
cd apps/business-m/frontendpnpm add ../../../libs/my_plugin/frontend5. Role-Based Menu Visibility (visibility_policy)
Section titled “5. Role-Based Menu Visibility (visibility_policy)”Every resource in your plugin manifest can be restricted to specific user roles using visibility_policy. The framework evaluates these on the frontend and shows or hides menu items accordingly.
Using Canonical Role Constants
Section titled “Using Canonical Role Constants”Always import role constants from the module’s roles file — never use raw string literals in visibility_policy. This ensures a CI contract test can validate that every referenced role is a registered canonical role.
import type { FrameworkMPlugin } from "@framework-m/plugin-sdk";import { WMSRoles } from "./roles";
// roles.ts — defines canonical string constantsexport const WMSRoles = { WAREHOUSE_MANAGER: "Warehouse Manager", WAREHOUSE_OPERATOR: "Warehouse Operator", INVENTORY_AUDITOR: "Inventory Auditor",} as const;
const plugin: FrameworkMPlugin = { name: "my_plugin", version: "0.1.0", manifests: [{ app_id: "my_plugin", resources: [ { name: "my_plugin.dashboard", label: "Dashboard", route: "/my-plugin/dashboard", // ✅ Correct — use constants from roles file visibility_policy: { roles: [WMSRoles.WAREHOUSE_MANAGER, WMSRoles.WAREHOUSE_OPERATOR] } }, { name: "my_plugin.admin", label: "Admin Setup", route: "/my-plugin/admin", visibility_policy: { // ✅ Also valid — string literals from the canonical class are checked roles: ["Warehouse Manager"] } } ] }]};Contract Test Enforcement
Section titled “Contract Test Enforcement”A CI test (tests/test_frontend_roles_contract.py) automatically parses all plugin.config.ts files and asserts that every role string in visibility_policy.roles[] exists in the canonical RoleRegistry. This means:
- You cannot add a new role to a menu without first registering it in the Python
RoleRegistry - Typos or renamed roles are caught at CI time, not at runtime
Performance: Bulk Authorization Checks
Section titled “Performance: Bulk Authorization Checks”The frontend evaluates visibility_policy by calling checkUserRole() for each menu resource. This uses a DataLoader pattern that coalesces all checks within a 20ms window into a single POST /authz/evaluate/bulk request, and caches results for the entire session. Adding many menu items to your plugin has zero performance cost after the first page load.
6. Development Tips
Section titled “6. Development Tips”- Namespacing: Always prefix your DocTypes and menu items to avoid collisions (e.g.,
my_plugin.settings). - Isolation: Use
m_protocolsto communicate with other libraries. Avoid direct imports of other domain logic. - Seeding: Use
m seedto populate initial data required by your plugin. - Roles: Define your role constants in
libs/my_plugin/src/my_plugin/roles.pyand register aRoleDefinitionin your module’sbootstrap.pybefore any DocTypeMetaclass is loaded.