Factorizer/Cluster ERP WikiCluster ERP Türkçe
Home / Platform

Explanation

Module lifecycle

How ModuleBase defines a Cluster ERP backend module and how ModuleRegistry discovers, loads, and shuts down the modules.

Every business feature of Cluster ERP is a module: a package under src/cluster_erp/modules/ with a module.py whose class Module subclasses ModuleBase. This page explains the contract in core/module_base.py and how core/module_registry.py discovers, loads, and shuts down modules. The modules themselves are documented in the modules section.

In short

  • ModuleBase declares three abstract properties (name, version, description) and five abstract methods (initialize, register_routes, register_events, shutdown, get_metadata) (src/cluster_erp/core/module_base.py:24-74).
  • A module's API prefix is /api/v1/<name> (src/cluster_erp/core/module_base.py:36-38).
  • The registry imports cluster_erp.modules.<name>.module, instantiates its Module class, and calls initialize, register_routes, and register_events in that order (src/cluster_erp/core/module_registry.py:32-56).
  • Modules are loaded in the order of the enabled_modules setting (src/cluster_erp/core/module_registry.py:58-72, src/cluster_erp/config/settings.py:535-564).
  • At shutdown the registry calls shutdown on every loaded module and logs, but does not raise, any error (src/cluster_erp/core/module_registry.py:74-80).

How it works

The contract

Member Kind What the registry or server expects Source
name abstract property The module key; also the key in registry.loaded src/cluster_erp/core/module_base.py:24-26
version abstract property A version string, shown by GET /api/v1/modules src/cluster_erp/core/module_base.py:28-30
description abstract property A one-line description, shown by GET /api/v1/modules src/cluster_erp/core/module_base.py:32-34
api_prefix property /api/v1/{name}, used by modules for their routers src/cluster_erp/core/module_base.py:36-38
request_database() method The current request's database when a request is in flight, else the module's own database handle src/cluster_erp/core/module_base.py:40-59
initialize(database) abstract async method Called first with the process default Database src/cluster_erp/core/module_base.py:61-62
register_routes(app) abstract method Adds the module's routers to the FastAPI app src/cluster_erp/core/module_base.py:64-65
register_events(bus) abstract method Subscribes event handlers and registers query handlers on the EventBus src/cluster_erp/core/module_base.py:67-68
shutdown() abstract async method Cleanup at process end src/cluster_erp/core/module_base.py:70-71
get_metadata() abstract method Returns the module's ModuleMeta src/cluster_erp/core/module_base.py:73-74

Discovery and loading

The state diagram shows the life of one module inside the server process.

stateDiagram-v2
 [*] --> Imported: import cluster_erp.modules.name.module
 Imported --> Instantiated: Module()
 Instantiated --> Initialized: initialize(database)
 Initialized --> Routed: register_routes(app)
 Routed --> Wired: register_events(bus)
 Wired --> Serving: stored in registry
 Serving --> Stopped: shutdown()
 Stopped --> [*]

In prose:

  1. discover() lists the sub-packages of cluster_erp.modules with pkgutil.iter_modules (src/cluster_erp/core/module_registry.py:26-30).
  2. load_all() takes enabled_modules when it is set, else the discovered list. A name that is enabled but not discovered is skipped with a warning (src/cluster_erp/core/module_registry.py:58-71).
  3. load_module() imports cluster_erp.modules.<name>.module. An import failure raises ModuleLoadError. A file without a Module class also raises ModuleLoadError (src/cluster_erp/core/module_registry.py:39-47).
  4. It instantiates Module() and awaits initialize(database), then calls register_routes(app) and register_events(bus) (src/cluster_erp/core/module_registry.py:49-52).
  5. It stores the instance under instance.name and logs the load (src/cluster_erp/core/module_registry.py:54-56).

load_all does not catch ModuleLoadError (src/cluster_erp/core/module_registry.py:58-72). The lifespan calls load_all without a handler (src/cluster_erp/server/app.py:140), so a failing module stops start-up.

What modules do in each step

  • initialize stores the database handle, imports the module's models, and may seed reference data. The parties module seeds channel types and countries (src/cluster_erp/modules/parties/module.py:37-42). The logs module only imports its models (src/cluster_erp/modules/logs/module.py:28-29).
  • register_routes builds an APIRouter with prefix=self.api_prefix and includes sub-module routers. 20 of the 28 module.py files pass require_app(self.name) as a router dependency (counted with grep -l "require_app("), for example src/cluster_erp/modules/currencies/module.py:64-82. App entitlements are documented in app store, licensing, and entitlements.
  • register_events wires handlers. See event bus.
  • shutdown is pass in 25 of the 28 modules (counted from the line after async def shutdown).

Who reads the metadata

get_metadata() is read at start-up to build tenant-field specs and to seed permissions (src/cluster_erp/server/app.py:151-152), and on request by GET /api/v1/_meta (src/cluster_erp/server/app.py:1531-1534). See metadata model.

Why it is built this way

  • The ModuleBase docstring states the recipe: create a package, add module.py with a class named Module, implement the abstract methods, and the module is discovered and loaded at start-up (src/cluster_erp/core/module_base.py:15-22).
  • The request_database docstring states why it exists: modules are initialised once with the default database, but in multi-tenant mode a request runs against its tenant's database, so a handler must use the request's database (src/cluster_erp/core/module_base.py:41-53).
  • The project's written engineering rules state the same four-step lifecycle.

Rules and boundaries

  • Every module folder has a module.py, and every module declares at least one route decorator. tests/test_architecture_boundaries.py:60-72 enforces both.
  • A module imports no other module except cluster_erp.modules.auth.guards. tests/test_architecture_boundaries.py:46-57 enforces this.

Interaction with other parts

  • The server creates the registry and calls load_all and shutdown_all in the lifespan (src/cluster_erp/server/app.py:63, src/cluster_erp/server/app.py:140, src/cluster_erp/server/app.py:220). See server startup and app factory.
  • GET /api/v1/modules returns the name, version, and description of every loaded module to a signed-in caller (src/cluster_erp/server/app.py:1506-1515).

Limits and open points

  • The registry has no dependency ordering between modules; the order is the list order. Whether any module depends on another module's initialize having run first is not determined from code.
  • registry.get(name) raises ModuleNotRegisteredError for an unknown name (src/cluster_erp/core/module_registry.py:82-86). Its callers were not inventoried for this page.

Terms introduced

Term Code identifier Definition Turkish term Source
module registry ModuleRegistry The object that discovers, loads, and shuts down modules. modül kaydı src/cluster_erp/core/module_registry.py:22
API prefix api_prefix The base path of a module's routes, /api/v1/ followed by the module name. API öneki src/cluster_erp/core/module_base.py:36-38

Verified against commit 92f8051 on 2026-11-13.

Verified against commit 92f8051 · 2026-11-13