Demo

Module lifecycle

How a backend plugin is packaged, loaded, and torn down — @Module registration plus lifecycle hooks @OnInit, @OnAllModuleLoaded, and @OnUninstall.

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.

TSXbackend/src/index.ts
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
TSXmodule
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.

TSX@OnInit on module
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}
TSX@OnInit on service
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.

TSX@OnAllModuleLoaded
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.

TSX@OnUninstall
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

  1. 01

    Scan installed plugins

    Only modules with installed=true load from installed-plugins.

  2. 02

    Import module.js

    Instantiate IPlugin and call getRootModule().

  3. 03

    DI + entities

    Wire services and sync tables.

  4. 04

    @OnInit

    Per-module (and service) init — seed and local setup.

  5. 05

    @OnAllModuleLoaded

    Cross-module hooks — workers, cron listeners.

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