Licensing
How module licensing and the MikoPBX marketplace work, at a high level — the author-facing model for commercial modules and version-filtered distribution.
This page explains, at an overview level, how module licensing and the MikoPBX marketplace fit together from a module author's point of view. It deliberately stays high-level: the license server, its endpoints, and the release infrastructure are internal MIKO services. What you need to know as an author is which keys go into your module.json, what the Core does with them at install and enable time, and how the marketplace decides which version of your module to offer a given PBX.
Throughout this page the running example is the fictional ModuleBlackList module (config class BlackListConf, main class BlackListMain, model BlackListNumbers). For a real, working commercial module you can read end to end, see Extensions/ModuleAmoCrm/. For a free module with no license keys at all, see Extensions/EXAMPLES/WebInterface/ModuleExampleForm/.
The two license keys in module.json
A commercial module declares its licensing identity with two integer keys in module.json:
lic_product_id
The product this module is sold as. Drives trial activation at install time.
lic_feature_id
The feature that must be present in the license key for the module to run (enable).
Here is the relevant slice of the real ModuleAmoCrm/module.json (Extensions/ModuleAmoCrm/module.json):
{
"developer": "MIKO",
"moduleUniqueID": "ModuleAmoCrm",
"support_email": "help@miko.ru",
"version": "%ModuleVersion%",
"min_pbx_version": "2022.2.1",
"lic_product_id": 81,
"lic_feature_id": 45
}For ModuleBlackList you would declare your own MIKO-assigned IDs, for example:
Product and feature IDs are issued by MIKO. You do not invent them. To sell a module through the marketplace you register the product with MIKO and receive the lic_product_id / lic_feature_id values to put in your module.json. Contact MIKO (help@miko.ru) to obtain them.
What the Core does at install time: activateLicense()
When a module is installed, PbxExtensionSetupBase::installModule() runs a fixed sequence of steps; the second of them is activateLicense(). See the base class at Core/src/Modules/Setup/PbxExtensionSetupBase.php.
The logic is short and worth reading in full:
In plain terms:
If
lic_product_idis0(a free module),activateLicense()does nothing and returnstrue. No license key is required to install.If
lic_product_id > 0, the PBX must already hold a license key (PbxSettings::PBX_LICENSE). With no key the install fails with theext_EmptyLicenseKeymessage, and becauseactivateLicense()returnsfalse,installModule()aborts the whole installation.With a key present, the Core asks the license service for a trial of the product via
$this->license->addtrial($this->lic_product_id). This requests a time-limited trial entitlement against the PBX's license key so the customer can evaluate the module immediately after install.
The $this->license worker is the MarketPlaceProvider shared service (MikoPBX\Common\Providers\MarketPlaceProvider), injected in the setup-base constructor. The underlying addtrial call talks to the MIKO license service — authors never call it directly and never need its internals.
For the full install pipeline (checkCompatibility() → activateLicense() → installFiles() → installDB() → fixFilesRights()), see the module installer page.
What happens at enable time: the feature gate and DISABLED_BY_LICENSE
Installing a module only places it on the system in a disabled state. The licensing decision that matters for day-to-day operation happens when the module is enabled, in PbxExtensionState::enableModule() (Core/src/Modules/PbxExtensionState.php).
If the module declares a lic_feature_id > 0, the Core asks the license service whether that feature is currently available before enabling anything:
When the feature is not available, the module stays disabled and the Core records why: it sets disableReason to the constant PbxExtensionState::DISABLED_BY_LICENSE and stores the human-readable explanation (translated via translateLicenseErrorMessage()) in disableReasonText. This is the mechanism by which a module that loses its license — for example, when the customer's license expires or the feature is revoked — is automatically taken out of service: the next enable attempt fails and the module is marked DISABLED_BY_LICENSE.
DISABLED_BY_LICENSE is one of a small set of disable-reason constants defined on PbxExtensionState:
So for ModuleBlackList: declaring lic_product_id means a customer cannot install it without a license key on the PBX, and a trial is requested on install; declaring lic_feature_id means each time the module is enabled the Core confirms the license still grants that feature, and quietly parks the module as DISABLED_BY_LICENSE if it does not.
Marketplace distribution and version filtering
The marketplace side answers a different question: which build of your module should a given PBX be offered?
Modules are published through the MIKO release pipeline, configured by the release_settings block in module.json. ModuleAmoCrm/module.json shows the real shape:
release_settings key
Effect
publish_release
Whether the build is published to the MIKO release server.
changelog_enabled
Whether a changelog is generated for the release.
create_github_release
Whether a corresponding GitHub release is created.
Once published, the MIKO release server lists the module and serves compatibility metadata for each release, including min_pbx_version and max_pbx_version. When a PBX queries the marketplace, the server uses that metadata to filter which releases are offered to that specific PBX version: a release whose compatibility window does not include the PBX's version is simply not presented.
Version filtering is server-side. The compatibility window (min_pbx_version / max_pbx_version) is enforced by the release server when it decides what to offer. The Core itself only checks the lower bound at install time: PbxExtensionSetupBase::checkCompatibility() compares the running PBX version against the module's min_pbx_version and refuses to install anything older than required. The Core does not enforce max_pbx_version locally — there is no max_pbx_version check in the install path. The upper bound exists purely as marketplace metadata for filtering offers.
For reference, here is the Core's only local version gate — note it is a one-sided min_pbx_version check:
In practice this means: set min_pbx_version honestly (the Core will hold you to it on every install), and rely on the release server's max_pbx_version metadata to stop offering an old build to PBX versions it was never tested against.
Author checklist
For a free module (like ModuleExampleForm):
Do not add
lic_product_idorlic_feature_id. Installation and enabling skip all licensing.You may still publish through the marketplace via
release_settings.
For a commercial module (like ModuleAmoCrm, or our ModuleBlackList):
Obtain
lic_product_idandlic_feature_idfrom MIKO (help@miko.ru).Add both keys to your
module.json.Set an accurate
min_pbx_version— the Core enforces it on install.Add a
release_settingsblock so the build is published and version-filtered by the release server.Test the loss-of-license path: with the feature revoked, your module should end up disabled with reason
DISABLED_BY_LICENSEand remain functional otherwise (no fatal errors in your config/main classes).
See also
module.json reference — every key, including
lic_product_id,lic_feature_id,min_pbx_version, andrelease_settings.Module installer — the full
installModule()pipeline and whereactivateLicense()andcheckCompatibility()run.Real commercial module:
Extensions/ModuleAmoCrm/.Real free module:
Extensions/EXAMPLES/WebInterface/ModuleExampleForm/.
Last updated
Was this helpful?