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¶
ModuleBasedeclares 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 itsModuleclass, and callsinitialize,register_routes, andregister_eventsin that order (src/cluster_erp/core/module_registry.py:32-56). - Modules are loaded in the order of the
enabled_modulessetting (src/cluster_erp/core/module_registry.py:58-72,src/cluster_erp/config/settings.py:535-564). - At shutdown the registry calls
shutdownon 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:
discover()lists the sub-packages ofcluster_erp.moduleswithpkgutil.iter_modules(src/cluster_erp/core/module_registry.py:26-30).load_all()takesenabled_moduleswhen 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).load_module()importscluster_erp.modules.<name>.module. An import failure raisesModuleLoadError. A file without aModuleclass also raisesModuleLoadError(src/cluster_erp/core/module_registry.py:39-47).- It instantiates
Module()and awaitsinitialize(database), then callsregister_routes(app)andregister_events(bus)(src/cluster_erp/core/module_registry.py:49-52). - It stores the instance under
instance.nameand 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¶
initializestores the database handle, imports the module's models, and may seed reference data. Thepartiesmodule seeds channel types and countries (src/cluster_erp/modules/parties/module.py:37-42). Thelogsmodule only imports its models (src/cluster_erp/modules/logs/module.py:28-29).register_routesbuilds anAPIRouterwithprefix=self.api_prefixand includes sub-module routers. 20 of the 28module.pyfiles passrequire_app(self.name)as a router dependency (counted withgrep -l "require_app("), for examplesrc/cluster_erp/modules/currencies/module.py:64-82. App entitlements are documented in app store, licensing, and entitlements.register_eventswires handlers. See event bus.shutdownispassin 25 of the 28 modules (counted from the line afterasync 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
ModuleBasedocstring states the recipe: create a package, addmodule.pywith a class namedModule, 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_databasedocstring 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-72enforces both. - A module imports no other module except
cluster_erp.modules.auth.guards.tests/test_architecture_boundaries.py:46-57enforces this.
Interaction with other parts¶
- The server creates the registry and calls
load_allandshutdown_allin 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/modulesreturns 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
initializehaving run first is not determined from code. registry.get(name)raisesModuleNotRegisteredErrorfor 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 |
Related pages¶
Verified against commit 92f8051 on 2026-11-13.