Overview
There are two layers. The core module is the primary host: it discovers modules, loads them in dependency order, and provides the basic features every ERP needs (auth, security, DI, routers, builtin data, menus, SPA chrome). Plugins are where you write your plugin apps — backend APIs, database tables, and React screens for a domain such as inventory, POS, or HR.
- Core = primary handler for module loading + basic platform features
- Plugins = self-contained apps you build under plugins/<name>/
- Loading is dynamic at boot / browser load — not one compile-time monolith
- Plugins must not import another plugin’s src/; use shared packages, metadata deps, and export/inject
1. Visual layout
At runtime the product is core plus whatever plugins are installed. Source lives under plugins/; builds stage under available-plugins/; only installed copies under installed-plugins/ are loaded by the core.
- Source — plugins/<name>/ — what you edit
- Available — base/available-plugins/<name>/<version>/ — quan-erp watch / build output
- Installed — base/backend/installed-plugins/<name>/ — what the core actually loads
- Folder name, metadata.name, and package identity must match
1base/ # core runtime (Docker)
2├── backend/ # core host process
3│ └── installed-plugins/<name>/ # active plugin copies
4├── frontend/ # SPA core (menus, chrome)
5└── available-plugins/<name>/<ver>/# staged builds
6
7plugins/<name>/ # your plugin app (edit here)
8├── backend/
9├── frontend/
10└── module.metadata.json # identity + dependencies2. module.metadata.json
Every plugin ships a module.metadata.json at the plugin root. The core reads it to know the module’s name, version, which base line it requires, and which other plugins must be present. Without correct metadata, discovery, install, and DI break.
- name is the key used in install rows, DI (@Inject(Service, "name")), and dependency maps
- requiredBasedVersion must align with the running core / @quan-erp/* line
- Keep metadata in sync when you rename a plugin or bump pluginVersion
- The same file is copied into available-plugins and installed-plugins builds
| Field | Purpose |
|---|---|
| name | Stable plugin id — must match folder name and install identity |
| pluginVersion | This plugin’s own semver (what dependents pin against) |
| description | Human-readable summary shown in module / install UI |
| moduleEntryObject | Backend entry export name (usually "Module") |
| requiredBasedVersion | Compatible core / @quan-erp/* base line (e.g. "1.0.0") |
| pluginDependencies | Map of other plugin names → semver ranges that must load first |
1{
2 "name": "inventory",
3 "type": "",
4 "pluginVersion": "1.0.0",
5 "description": "Inventory management",
6 "moduleEntryObject": "Module",
7 "requiredBasedVersion": "1.0.0",
8 "pluginDependencies": {
9 "products": "^1.0.0",
10 "accounting": "^1.0.0"
11 }
12}3. Module dependencies
pluginDependencies tell the core which other plugins this module needs and in what version range. The core resolves a topological load order: dependency plugins boot and register before dependents. Missing or incompatible dependencies block a healthy start.
- Keys are provider metadata.name values (not npm package names)
- Values are semver ranges for the provider’s pluginVersion
- Declare a dependency when you @Inject(Service, "other-plugin") or reuse its exported APIs
- Empty object {} means no plugin deps — core builtins are still available
- Do not import another plugin’s src/; declare the dep and use export / inject
"pluginDependencies": {
"products": "^1.0.0", // provider name → semver of its pluginVersion
"accounting": "^1.0.0"
}4. How the core loads modules
At startup the core combines filesystem builds with the module database table. Only rows marked installed are activated. For each installed plugin in dependency order it loads the backend, then the browser loads the frontend entry so the plugin can register into the shared AppRegistry.
- A build under available-plugins is not live until Install copies it to installed-plugins
- Property @Inject only — constructor injection is unsupported
- See Documentation → Backend → Module lifecycle and Frontend → Plugins lifecycle for deep dives
- 01
Discover
Read module.metadata.json from installed-plugins and match against the module table (installed / active).
- 02
Order
Build a dependency graph from pluginDependencies; load providers before consumers.
- 03
Backend
Load IPlugin entry, register @Module (providers, controllers, entities), run DI, then @OnInit / @OnAllModuleLoaded.
- 04
Frontend
Serve installed frontend assets, dynamic-import the plugin entry, call register(AppRegistry) once to inject routes, menus, and slots.
5. Next steps
Run the core stack, scaffold a plugin, and iterate with the CLI watch / install loop.
- Never import another plugin’s src/
- Align @quan-erp/* and requiredBasedVersion with the running core
- Keep folder name = metadata.name consistent
- List real providers in pluginDependencies before cross-plugin inject