Demo

How it works

Quark ERP is a core module that loads plugins and ships basic platform features, plus plugin apps you build for domain work. Metadata and pluginDependencies decide identity, compatibility, and boot order.

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
SHproject layout
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 + dependencies

2. 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
FieldPurpose
nameStable plugin id — must match folder name and install identity
pluginVersionThis plugin’s own semver (what dependents pin against)
descriptionHuman-readable summary shown in module / install UI
moduleEntryObjectBackend entry export name (usually "Module")
requiredBasedVersionCompatible core / @quan-erp/* base line (e.g. "1.0.0")
pluginDependenciesMap of other plugin names → semver ranges that must load first
JSONplugins/inventory/module.metadata.json
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
TSdependency map shape
"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
  1. 01

    Discover

    Read module.metadata.json from installed-plugins and match against the module table (installed / active).

  2. 02

    Order

    Build a dependency graph from pluginDependencies; load providers before consumers.

  3. 03

    Backend

    Load IPlugin entry, register @Module (providers, controllers, entities), run DI, then @OnInit / @OnAllModuleLoaded.

  4. 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