Skip to main content

Tutorial: Building a Multi-Module App

Build a WMS (Warehouse Management) + Personnel app using the Framework M plugin system.

Prerequisites

  • Framework M workspace set up (pnpm install done)
  • Node.js 20+, Python 3.12+

Step 1: Create the WMS App

m new:app wms --with-frontend

This creates both the Python backend and frontend plugin:

wms/
├── pyproject.toml
├── src/wms/doctypes/
├── frontend/
│ ├── package.json # framework-m plugin metadata
│ └── src/
│ └── plugin.config.ts # Plugin manifest

Step 2: Define a DocType

Create a DocType for inventory items:

m new:doctype InventoryItem --app wms

Edit the generated DocType to add fields (refer to Defining DocTypes).

Step 3: Add Frontend Pages

Create a dashboard page:

// wms/frontend/src/pages/Dashboard.tsx
import { useDocTypes } from "@framework-m/desk";

export default function WmsDashboard() {
const { resources } = useDocTypes();

return (
<div style={{ padding: "2rem" }}>
<h1>WMS Dashboard</h1>
<div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: "1rem" }}>
<div className="card">
<h3>Inventory Items</h3>
<p>Manage stock levels and locations</p>
</div>
<div className="card">
<h3>Receiving</h3>
<p>Process incoming shipments</p>
</div>
<div className="card">
<h3>Picking</h3>
<p>Fulfill orders from warehouse</p>
</div>
</div>
</div>
);
}

Create an inventory list page:

// wms/frontend/src/pages/Inventory.tsx
import { AutoTable, useDocTypeMeta } from "@framework-m/desk";

export default function Inventory() {
const { schema } = useDocTypeMeta("InventoryItem");

return (
<div style={{ padding: "2rem" }}>
<h1>Inventory</h1>
<AutoTable resource="InventoryItem" schema={schema} />
</div>
);
}

Step 4: Register Routes and Menus

Edit the plugin manifest:

// wms/frontend/src/plugin.config.ts
import type { FrameworkMPlugin } from "@framework-m/plugin-sdk";

const plugin: FrameworkMPlugin = {
name: "wms",
version: "0.1.0",

menu: [
{
name: "wms.dashboard",
label: "WMS Dashboard",
route: "/wms/dashboard",
icon: "warehouse",
module: "WMS",
category: "Overview",
order: 1,
},
{
name: "wms.inventory",
label: "Inventory",
route: "/wms/inventory",
icon: "package",
module: "WMS",
category: "Operations",
order: 2,
},
],

routes: [
{
path: "/wms/dashboard",
element: () => import("./pages/Dashboard"),
},
{
path: "/wms/inventory",
element: () => import("./pages/Inventory"),
},
],
};

export default plugin;

Step 5: Add a Second Module (Personnel)

m new:app personnel --with-frontend

Edit personnel/frontend/src/plugin.config.ts:

import type { FrameworkMPlugin } from "@framework-m/plugin-sdk";

const plugin: FrameworkMPlugin = {
name: "personnel",
version: "0.1.0",

menu: [
{
name: "personnel.directory",
label: "Employee Directory",
route: "/personnel/directory",
icon: "users",
module: "Personnel",
order: 1,
},
],

routes: [
{
path: "/personnel/directory",
element: () => import("./pages/Directory"),
},
],
};

export default plugin;

Step 6: Run

pnpm install
pnpm dev

Both plugins are auto-discovered. The sidebar shows:

DOCTYPES
├── Core: User, Role, ...
──────────────────────
WMS
├── Overview: WMS Dashboard
├── Operations: Inventory
PERSONNEL
├── Employee Directory

Step 7: Cross-Plugin Services (Optional)

Register a service in the WMS plugin:

// wms plugin.config.ts
services: {
inventoryService: () => import("./services/InventoryService"),
}

Consume it from the Personnel plugin:

// personnel/frontend/src/pages/Directory.tsx
import { useService } from "@framework-m/plugin-sdk";

export default function Directory() {
const { service: inventory } = useService("inventoryService");
// Access WMS inventory data from Personnel module
}

What's Happening Under the Hood

  1. Build time: @framework-m/vite-plugin scans workspace, finds both plugins' package.json with "framework-m" metadata
  2. Virtual module: Generates virtual:framework-m-plugins importing both plugin configs
  3. App start: App.tsx calls bootstrapPlugins() → registers both in PluginRegistry
  4. Rendering: PluginRegistryProvider makes registry available → Sidebar uses usePluginMenu() → Routes rendered via PluginRoutes

Next Steps