Factorizer/Cluster ERP WikiCluster ERP Türkçe
Home / Identity and access

Explanation

App store licensing and entitlements

How the app_store module, the licence file, role assignments, and server/entitlements.py decide which apps and module APIs a user may use.

Cluster ERP sells its business areas as apps. Whether a user can open an app, and whether the API of a backend module answers, depends on four layers: the catalogue, the licence, role assignments, and the deployment's install state. The app_store module in src/cluster_erp/modules/app_store/ keeps these records, and server/entitlements.py enforces them on every request to a gated module. This page explains each layer at mechanism level. It does not describe licence file contents or verification details. The module overview is on the app store module page.

In short

  • The catalogue is shipped in modules/app_store/catalog.json and converged into app_store_apps at every start-up; it lists 28 apps, 6 of them system apps (src/cluster_erp/modules/app_store/services.py:50-97, counted from catalog.json entries with system or permanent set).
  • The licence is a vendor-signed document uploaded by an administrator and re-verified against the public key in license_public_key on every resolution (src/cluster_erp/modules/app_store/license.py:1-14, src/cluster_erp/modules/app_store/services.py:201-236).
  • Licence states are none, invalid, active, grace, and read_only (src/cluster_erp/modules/app_store/license.py:9-14, src/cluster_erp/modules/app_store/license.py:33).
  • Role assignments use three intents, required, available, and blocked; an app without assignment rows is open to everyone (src/cluster_erp/modules/app_store/models.py:77-104, src/cluster_erp/modules/app_store/services.py:23-47).
  • Business modules attach require_app(self.name) to their routers, which refuses requests when no installed, licensed, and seated app covers the module (src/cluster_erp/server/entitlements.py:225-273, for example src/cluster_erp/modules/bom/module.py:59).

How it works

The layers

Layer Record Question Source
L0 catalogue StoreApp in app_store_apps Which apps exist, which backend modules each covers, which are system apps src/cluster_erp/modules/app_store/models.py:14-52
L1 licence AppLicense in app_store_licenses Which apps the customer has paid for, and whether the licence is current src/cluster_erp/modules/app_store/models.py:55-75
L2 seat AppAssignment in app_store_assignments Which roles may use an app src/cluster_erp/modules/app_store/models.py:77-104
L3 install AppInstall in app_store_installs Whether the deployment has the app installed, and which modules the administrator consented to src/cluster_erp/modules/app_store/models.py:123-162

A user's request for an app (AppRequest, statuses pending, approved, denied) asks an administrator to grant a seat; approval makes the app available for the requester's roles (src/cluster_erp/modules/app_store/models.py:107-120, src/cluster_erp/modules/app_store/services.py:335-361).

The entitlement gate

The flowchart shows assert_module_entitled for one request.

flowchart TD
    req["request to a gated module"] --> resolve["_resolve: guarded, installed, entitled, licence status"]
    resolve --> sold{"module sold by any app?"}
    sold -->|no| pass["pass"]
    sold -->|yes| seat{"user holds a seat?"}
    seat -->|no| s403["entitlement.no_seat 403"]
    seat -->|yes| inst{"module installed?"}
    inst -->|no| i403["entitlement.module_not_installed 403"]
    inst -->|yes| lic{"module licensed?"}
    lic -->|no| l403["entitlement.app_not_licensed 403"]
    lic -->|yes| ro{"read_only and a write?"}
    ro -->|yes| r403["entitlement.license_read_only 403"]
    ro -->|no| pass

In prose:

  1. _resolve computes the modules that any catalogue app covers, the modules covered by installed apps within the administrator's consent, the modules covered by licensed apps, and the licence status (src/cluster_erp/server/entitlements.py:80-186).
  2. A module no app covers passes freely (src/cluster_erp/server/entitlements.py:232-233).
  3. The user must hold a seat on some app that covers the module (src/cluster_erp/server/entitlements.py:189-222, src/cluster_erp/server/entitlements.py:234-240).
  4. The module must be installed and licensed (src/cluster_erp/server/entitlements.py:241-250).
  5. In the read_only licence state, only GET, HEAD, and OPTIONS pass (src/cluster_erp/server/entitlements.py:251-257).

Each refusal carries a message code, not display text, so the client can show a localised card (src/cluster_erp/server/entitlements.py:260-265). Generic record lifecycle routes that carry the module name in the path call the same check (src/cluster_erp/modules/records/lifecycle_routes.py:130).

Licence states and the licensing posture

State Meaning Source
none No licence uploaded, or no public key configured src/cluster_erp/modules/app_store/services.py:209-210
invalid The document does not verify or is malformed src/cluster_erp/modules/app_store/license.py:52-62
active Before the expiry date src/cluster_erp/modules/app_store/license.py:76-77
grace Expired, within the grace days the licence grants; full function src/cluster_erp/modules/app_store/license.py:78-79
read_only Expired past grace; writes are refused src/cluster_erp/modules/app_store/license.py:80-81

When license_public_key is empty, licensing is off and every catalogue app is entitled. When the key is set, a missing or invalid licence entitles only system apps (src/cluster_erp/server/entitlements.py:158-173, src/cluster_erp/modules/app_store/services.py:404-417, src/cluster_erp/config/settings.py:566-569). An upload that does not verify is refused with HTTP 422, and a replaced licence stays on record as a deactivated row (src/cluster_erp/modules/app_store/services.py:238-265). The web screen shows the state as License Status (webapp/i18n/en.json key license.status_title).

  • At first start-up every catalogue app is installed, and the install row records the app version and the modules consented to (src/cluster_erp/modules/app_store/services.py:114-159).
  • When an update widens an app's module list, the new modules stay closed until an administrator re-consents (src/cluster_erp/modules/app_store/services.py:161-180, src/cluster_erp/server/entitlements.py:153-157).
  • Uninstall sets the status to uninstalled and never deletes module data; system apps cannot be uninstalled (src/cluster_erp/modules/app_store/models.py:123-136, src/cluster_erp/modules/app_store/services.py:182-189).
  • Per-app JavaScript bundles live in gated_apps/ outside the public static mount and are served by GET /api/v1/app_store/assets/{app_key}/{filename} after a layer check for the user (src/cluster_erp/modules/app_store/routes.py:95-155). Nine folders exist in gated_apps/. See gated app bundles.
  • GET /api/v1/app_store/registry returns, for the signed-in user, every enabled app with its entitled, installed, seat, and reconsent state; the web client renders its launcher from it (src/cluster_erp/modules/app_store/routes.py:18-39, src/cluster_erp/modules/app_store/services.py:375-447).

Permissions

Code Used by Source
license.manage Upload and remove the licence src/cluster_erp/modules/app_store/routes.py:71-92
store_app.assign Role assignments and app requests src/cluster_erp/modules/app_store/routes.py:158-264
store_app.install Install and reconsent src/cluster_erp/modules/app_store/routes.py:267-285
store_app.uninstall Uninstall src/cluster_erp/modules/app_store/routes.py:288-295

The codes are seeded as extra codes (src/cluster_erp/server/app.py:801-807).

Why it is built this way

  • The licence row is a cache and the file is re-verified on every resolution because a self-hosting administrator can edit the database but cannot forge a signature (src/cluster_erp/modules/app_store/license.py:3-7).
  • read_only keeps data readable and exportable because an ERP never holds business data hostage (src/cluster_erp/modules/app_store/license.py:9-11).
  • The gate is the security boundary; hiding a tile in the client is only presentation (src/cluster_erp/server/entitlements.py:4-8).
  • With licensing armed, a deleted licence closes the system instead of opening it (src/cluster_erp/server/entitlements.py:158-163).
  • Scope growth needs a new consent so that an update never silently widens an app's access (src/cluster_erp/modules/app_store/models.py:153-157).

Rules and boundaries

  • The resolved state is cached in process memory, keyed by a version stamp that database triggers move on install and licence changes; every install mutation also drops the cache (src/cluster_erp/server/entitlements.py:32-77, src/cluster_erp/modules/app_store/routes.py:82).
  • tests/test_entitlement_stamp.py (2 tests) and tests/test_store_catalog_assets.py (4 tests) pin the stamp and the catalogue assets; e2e suites include web-client/_e2e_app_store.mjs, web-client/_e2e_license.mjs, and web-client/_e2e_store.mjs.

Interaction with other parts

  • Role ids for seats come from the auth guards door, role_ids_for (src/cluster_erp/modules/app_store/routes.py:250-252, src/cluster_erp/modules/auth/guards.py:189-202).
  • The gallery pages of the store are in webapp/store/, one folder per app (26 folders). See gated app bundles.

Limits and open points

  • The seat and entitled values in the registry docstring are described as future phases, but the code computes both today (src/cluster_erp/modules/app_store/services.py:381-387, src/cluster_erp/modules/app_store/services.py:410-441).
  • Licence issuance on the vendor side is not part of this repository's server code; how licence files are produced is not determined from code.

Terms introduced

Term Code identifier Definition Turkish term Source
catalogue app StoreApp One sellable app with the backend modules it covers. katalog uygulaması src/cluster_erp/modules/app_store/models.py:14
licence state LicenseState The derived state of the uploaded licence: none, invalid, active, grace, or read_only. Lisans Durumu (webapp/i18n/tr.json key license.status_title) src/cluster_erp/modules/app_store/license.py:32
seat assignment AppAssignment A rule that makes an app required, available, or blocked for a role. koltuk ataması src/cluster_erp/modules/app_store/models.py:77
install consent consented_modules The backend modules an administrator approved for an installed app. kurulum onayı src/cluster_erp/modules/app_store/models.py:162
entitlement gate require_app The router dependency that refuses requests to an unentitled module. hak kapısı src/cluster_erp/server/entitlements.py:260

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

Verified against commit 92f8051 · 2026-11-13