Mental model
Think in three layers. plugins/ is where you write code. base/available-plugins/ is the staged build. base/backend/installed-plugins/ is what the running ERP actually loads. The quan-erp CLI copies from source → available; the ERP UI copies available → installed.
- Always run quan-erp from the repo root so plugins/ and base/ resolve correctly
- Folder name under plugins/ must equal module.metadata.json name
- Package names: @quan-erp-plugins/<name>-backend and @quan-erp-plugins/<name>-frontend
- Never import another plugin’s src/ — use shared @quan-erp/* libs or PluginAPI
1. Repository root
Clone of quan-erp-node-template (or your fork). Top-level layout:
- base/ — platform core you run with Docker Compose; do not put feature business code here
- plugins/ — every custom module; start from plugins/sample-es
- .agents/ — Cursor / agent skills and rules for Quark ERP conventions
- erp — prefer quan-erp watch <name> over hand-copying dist folders
1my-quan-erp/ # repo root (run quan-erp here)
2├── erp # CLI binary — watch / build / pack / new
3├── .agents/ # Quan ERP skills & rules for AI agents
4├── .gitignore
5├── README.md
6├── base/ # runnable platform core (Docker)
7│ ├── docker-compose.yaml # db, redis, backend, frontend
8│ ├── available-plugins/ # built plugins waiting to install
9│ │ └── <name>/<version>/
10│ │ ├── backend/
11│ │ ├── frontend/
12│ │ └── module.metadata.json
13│ ├── backend/
14│ │ ├── Dockerfile
15│ │ ├── .env / .env.sample # local secrets (do not commit real values)
16│ │ ├── installed-plugins/ # active plugins served at runtime
17│ │ │ └── <name>/
18│ │ │ ├── backend/
19│ │ │ ├── frontend/
20│ │ │ └── module.metadata.json
21│ │ └── logs/ # optional host-mounted logs
22│ ├── frontend/ # base UI core image / nginx
23│ └── data/ # optional local volumes (db, redis, uploads)
24└── plugins/ # YOUR source code lives here
25 ├── sample-es/ # reference plugin — copy patterns from here
26 └── <my-plugin>/ # one folder per plugin
27 ├── module.metadata.json # name MUST match folder name
28 ├── backend/
29 └── frontend/2. base/ — platform core
The base app is a thin runtime: compose services, plugin folders, and host config. Feature ERP screens live in plugins, not in base frontend source.
- docker-compose.yaml — Postgres (db), Redis, backend (:8080/:8081), frontend (:80)
- available-plugins/<name>/<version>/ — output of quan-erp watch|build:prod|pack:prod staging
- backend/installed-plugins/<name>/ — plugins marked installed; backend + browser load from here
- backend/.env — DATABASE_*, REDIS_*, tokens; keep real secrets out of git
- frontend/ — base core image / static host; plugins inject UI at runtime
- data/ — optional bind mounts for db/redis/uploads on local machines
3. plugins/<name>/ — plugin package
One plugin = one folder with metadata + backend package + frontend package.
- module.metadata.json sits beside backend/ and frontend/ (not inside either package)
- Keep domain code under feature folders — avoid dumping everything in src/
- output/ may appear after quan-erp pack:prod (zip archives); safe to gitignore
1plugins/<my-plugin>/
2├── module.metadata.json # identity + deps + version contract
3├── backend/
4│ ├── package.json # @quan-erp-plugins/<name>-backend
5│ ├── tsconfig.json
6│ ├── nest-cli / build config # as shipped by the template
7│ └── src/
8│ ├── index.ts # IPlugin default export → getRootModule()
9│ ├── export.ts # optional: publish for other plugins
10│ ├── <feature>/ # one folder per domain (recommended)
11│ │ ├── *.controller.ts
12│ │ ├── *.service.ts
13│ │ └── dto/
14│ └── schema/ # TypeORM entities (plugin tables)
15│ └── *.entity.ts
16└── frontend/
17 ├── package.json # @quan-erp-plugins/<name>-frontend
18 ├── vite.config.ts
19 ├── tsconfig.json
20 ├── index.html
21 └── src/
22 ├── index.tsx # PluginModule.register(AppRegistry)
23 ├── export.ts # public API for PluginAPI consumers
24 ├── api/<domain>/ # React Query + axios calls
25 ├── page/<feature>/ # screens / dialogs
26 ├── components/ # plugin-local UI pieces
27 ├── hooks/ # plugin-local hooks (optional)
28 ├── store/ # plugin-local Zustand (optional)
29 ├── locales/ # translation dictionaries (optional)
30 └── lib/
31 ├── axios.ts # setAxiosClient / getAxiosClient
32 └── metadata.ts # typed module.metadata.json4. module.metadata.json
Identity file read by the CLI, base discovery, and UI install flow.
- name — unique plugin id; must match plugins/<name>/ folder
- pluginVersion — folder name under available-plugins/<name>/<version>/
- requiredBasedVersion — must align with base / @quan-erp/* version line
- moduleEntryObject — root @Module class name exported from backend entry
- pluginDependencies — other plugin names that must load first
1{
2 "name": "sample-es",
3 "type": "",
4 "pluginVersion": "1.0.0",
5 "description": "Sample plugin",
6 "moduleEntryObject": "Module",
7 "requiredBasedVersion": "1.0.0",
8 "pluginDependencies": {}
9}5. Backend layout (detail)
Backend is a TypeScript package that default-exports an IPlugin. Controllers, services, DTOs, and entities stay namespaced to the plugin.
- index.ts — only backend entry the host imports (module.js after build)
- schema/ — TypeORM entities; prefix table names with the plugin namespace
- dto/ — validation / transport types; keep controllers thin
- export.ts — optional publish surface for cross-plugin backend reuse
- Do not reach into another plugin’s backend/src
1plugins/<name>/backend/src/
2├── index.ts
3│ # default-export class implements IPlugin
4│ # getRootModule() → @Module class
5│ # getMetadata() → module.metadata.json
6├── export.ts
7│ # optional — symbols other plugins may consume after publish
8├── <feature>/ # e.g. employee/, leave/, payroll/
9│ ├── <feature>.controller.ts # @Controller + routes
10│ ├── <feature>.service.ts # @Service business logic
11│ ├── dto/
12│ │ ├── create-*.dto.ts
13│ │ └── query-*.dto.ts
14│ └── <feature>.module bits # providers registered on root @Module
15└── schema/
16 ├── <feature>.entity.ts # @Entity — namespace table names
17 └── index.ts # re-export entities for @Module({ entities })6. Frontend layout (detail)
Frontend is a Vite React app. The core only loads src/index.tsx and calls register(AppRegistry).
- index.tsx — menus, routes, dashboards, settings, reports, PluginAPI.expose, …
- api/ — all HTTP + React Query; pages import hooks, not raw axios sprawl
- page/ — route-level screens; keep feature files together
- lib/axios.ts — setAxiosClient(AppRegistry.getAxiosClient()) inside register()
- lib/metadata.ts — read name/version/deps from module.metadata.json
- export.ts — only stable selectors / hooks / types other plugins may use
1plugins/<name>/frontend/src/
2├── index.tsx
3│ # default-export PluginModule
4│ # register(AppRegistry): wire axios, menus, routes, dashboards, …
5├── export.ts
6│ # stable public surface for PluginAPI.expose / consumers
7├── api/
8│ └── <domain>/
9│ ├── <domain>.api.ts # axios + React Query hooks
10│ └── types.ts # request/response types
11├── page/
12│ └── <feature>/
13│ ├── <feature>.page.tsx # route screen
14│ ├── <feature>-form.tsx
15│ └── columns.tsx # table defs (optional)
16├── components/ # shared inside this plugin only
17├── hooks/
18├── store/
19├── locales/
20│ ├── en.json
21│ └── mm.json
22└── lib/
23 ├── axios.ts # host client bridge
24 └── metadata.ts # name / version / dependencies7. Build & install artifact paths
Source, available, and installed are different directories. If the UI is blank after install, check installed-plugins and the module table — not only plugins/.
- quan-erp watch|build:prod → writes base/available-plugins/<name>/<version>/
- ERP UI Install → copies into base/backend/installed-plugins/<name>/
- If already installed, watch also refreshes the installed copy during development
- quan-erp pack:prod → zip under plugins/<name>/output (or a path you pass)
1# Source (edit here)
2plugins/<name>/{backend,frontend,module.metadata.json}
3
4# After quan-erp watch|build:prod → staged for install
5base/available-plugins/<name>/<version>/
6├── backend/ # compiled backend dist
7├── frontend/ # Vite build output
8└── module.metadata.json
9
10# After ERP UI Install → active at runtime
11base/backend/installed-plugins/<name>/
12├── backend/
13├── frontend/
14└── module.metadata.json8. Layout rules
- No cross-plugin direct imports from another plugin’s src/
- Namespace DB tables, API routes, menu keys, and registry ids with the plugin name
- Keep API logic in src/api — not inside page components
- Do not edit base/available-plugins or installed-plugins by hand — use quan-erp CLI + UI install
- Align @quan-erp/* versions in both backend and frontend package.json with the base images
- Treat sample-es as the canonical layout reference when unsure