IPlugin entry
backend/src/index.ts default-exports a class that implements IPlugin. It returns the root @Module class and metadata from module.metadata.json.
1import type { IPlugin, PluginMetadata } from "@quan-erp/shared-types";
2import metadata from "../module.metadata.json" with { type: "json" };
3import { MyPluginModule } from "./my-plugin.module.js";
4
5export default class MyPlugin implements IPlugin {
6 getRootModule() { return MyPluginModule; }
7 getName() { return metadata.name; }
8 getVersion() { return metadata.pluginVersion; }
9 getMetadata() { return metadata as PluginMetadata; }
10}Root @Module
Register providers, controllers, entities (plugin: "default"), and optional websocket handlers. Pair with @Cache on the same class for module-level cache. Lifecycle hooks usually live on this class (or on a @Service).
- @Module({ name, providers, controllers, entities, websocket? })
- @Cache(options) — in-memory or redis; must sit on the @Module class only
1import { Cache, Module } from "@quan-erp/shared-backend-core";
2import metadata from "../../module.metadata.json" with { type: "json" };
3import { ChatWebsocket } from "./chat.websocket.js";
4import { MyController } from "./my.controller.js";
5import { MyEntity } from "./my.entity.js";
6import { MyService } from "./my.service.js";
7
8@Module({
9 name: metadata.name,
10 providers: [MyService],
11 controllers: [MyController],
12 entities: [{ plugin: "default", entities: [MyEntity] }],
13 websocket: [ChatWebsocket],
14})
15@Cache({ type: "in-memory", checkperiod: 1000 })
16export class MyPluginModule {}Lifecycle annotations
Three hooks cover boot and teardown. Use property methods on the root @Module (or a DI-managed @Service). Constructor injection is not supported — inject dependencies with property @Inject / @InjectBuiltinLogger.
- @OnInit() — this module (or service) is ready; safe for seed / local setup
- @OnAllModuleLoaded() — every module finished @OnInit; safe for workers, cron listeners, cross-plugin calls
- @OnUninstall() — plugin is being removed; disconnect queues, stop workers, cleanup resources
@OnInit
Runs once after the system has fully initialized the host module or service. Typical uses: default settings, COA/seed via DataSeedHistoryService, one-time local setup. Do not start cross-plugin workers here — use @OnAllModuleLoaded instead.
1import {
2 ContainerRegistryManager,
3 DataSeedHistoryService,
4 Inject,
5 InjectBuiltinLogger,
6 Module,
7 OnInit,
8} from "@quan-erp/shared-backend-core";
9import type { Loggable } from "@quan-erp/shared-backend-core";
10import metadata from "../../module.metadata.json" with { type: "json" };
11import { MyService } from "./my.service.js";
12
13@Module({
14 name: metadata.name,
15 providers: [MyService],
16 controllers: [],
17 entities: [],
18})
19export class MyPluginModule {
20 @InjectBuiltinLogger()
21 logger: Loggable;
22
23 @Inject(DataSeedHistoryService, ContainerRegistryManager.BUILTIN_PLUGIN)
24 dataSeedHistoryService: DataSeedHistoryService;
25
26 @OnInit()
27 async init() {
28 const alreadySeeded = await this.dataSeedHistoryService.find(
29 metadata.name,
30 metadata.pluginVersion,
31 "init",
32 );
33 if (alreadySeeded) {
34 this.logger.log("Seeding skipped (already initialized)");
35 return;
36 }
37 // … seed defaults …
38 await this.dataSeedHistoryService.add({
39 data: {
40 pluginName: metadata.name,
41 pluginVersion: metadata.pluginVersion,
42 name: "init",
43 },
44 });
45 this.logger.log("Module seeded successfully");
46 }
47}1import { InjectBuiltinLogger, OnInit, Service } from "@quan-erp/shared-backend-core";
2import type { Loggable } from "@quan-erp/shared-backend-core";
3
4@Service()
5export class CacheWarmupService {
6 @InjectBuiltinLogger()
7 logger: Loggable;
8
9 @OnInit()
10 async init() {
11 this.logger.log("Warming local caches");
12 // …
13 }
14}@OnAllModuleLoaded
Runs once after every installed module has finished @OnInit. Use for background workers, CronJobService event listeners, or any work that needs other plugins’ services to be available.
1import {
2 ContainerRegistryManager,
3 CronJobService,
4 Inject,
5 InjectBuiltinLogger,
6 Module,
7 OnAllModuleLoaded,
8} from "@quan-erp/shared-backend-core";
9import type { Loggable } from "@quan-erp/shared-backend-core";
10import metadata from "../../module.metadata.json" with { type: "json" };
11import { MaturedSweepService } from "./matured-sweep.service.js";
12
13@Module({
14 name: metadata.name,
15 providers: [MaturedSweepService],
16 controllers: [],
17 entities: [],
18})
19export class MyPluginModule {
20 @InjectBuiltinLogger()
21 logger: Loggable;
22
23 @Inject(CronJobService, ContainerRegistryManager.BUILTIN_PLUGIN)
24 cronJobService: CronJobService;
25
26 @Inject(MaturedSweepService)
27 sweep: MaturedSweepService;
28
29 @OnAllModuleLoaded()
30 async onAllModuleLoaded() {
31 this.logger.log("All modules loaded — attaching cron listeners");
32 this.cronJobService.addEventListener(
33 metadata.name,
34 "matured-sweep",
35 () => this.sweep.run(),
36 );
37 }
38}@OnUninstall
Runs when the plugin is uninstalled from the system. Disconnect Redis/BullMQ workers, close sockets, and release other resources you started in @OnInit / @OnAllModuleLoaded.
1import {
2 InjectBuiltinLogger,
3 Module,
4 OnUninstall,
5} from "@quan-erp/shared-backend-core";
6import type { Loggable } from "@quan-erp/shared-backend-core";
7import metadata from "../../module.metadata.json" with { type: "json" };
8import { BackgroundWorker } from "./background.worker.js";
9
10@Module({
11 name: metadata.name,
12 providers: [BackgroundWorker],
13 controllers: [],
14 entities: [],
15})
16export class MyPluginModule {
17 @InjectBuiltinLogger()
18 logger: Loggable;
19
20 private worker = new BackgroundWorker();
21
22 @OnUninstall()
23 async cleanup() {
24 this.logger.log("Uninstalling — stopping worker");
25 await this.worker.disconnect();
26 }
27}Boot & teardown order
- 01
Scan installed plugins
Only modules with installed=true load from installed-plugins.
- 02
Import module.js
Instantiate IPlugin and call getRootModule().
- 03
DI + entities
Wire services and sync tables.
- 04
@OnInit
Per-module (and service) init — seed and local setup.
- 05
@OnAllModuleLoaded
Cross-module hooks — workers, cron listeners.
- 06
@OnUninstall
When the plugin is removed — cleanup resources.
Build lifecycle
Use quan-erp watch <plugin> (or quan-erp watch in some repos) to compile backend + frontend into base/available-plugins/, then install from the ERP UI.