Versioning
What the Light API promises, what a release may change, and how your module declares the versions it supports.
Status: public beta (Light API 0.1.0, tag light-api-0.1.0).
Light promises that a module built on one Light API version keeps working on every later minor of the same major. That promise starts at 1.0.0; during the 0.x public beta the 0.x rule below holds instead. This page says what the promise covers, what a release may change, how a module states the versions it supports, and what Light does with a module that doesn't fit.
0.x, the public beta and 1.0.0
Mrx\Light\Api\ExtensionApi::VERSIONis the Light API version.0.1.0is the public beta: the first published API, released for a testing phase. 1.0.0 follows after that testing.- The 0.x rule. While the API is 0.x, a minor release such as 0.2.0 may break, and a patch release such as 0.1.1 never does. Patch releases are package-only, so
VERSIONstaysx.y.0. - The surface snapshot,
app/code/Mrx/Light/Test/Unit/Api/_files/surface.json, has a fieldreleased: the last published API version. It wasnullbefore the beta, is0.1.0from the beta launch and becomes1.0.0at the 1.0.0 release. There is no separate marker file. - What
^0.4means. Composer reads a caret on a 0.x version as a pin on the minor:^0.4is>=0.4.0 <0.5.0. So a module on^0.4is out of range on 0.5.0 and never matches 1.0.0, and at the 1.0.0 release every range in this repository moves from^0.4to^1.0. From 1.0.0 the caret pins the major:^1.0is>=1.0.0 <2.0.0, so a module stays in range across minors. @sincetags say0.1.0on everything the beta publishes, and so does thesincecolumn of the reference pages. A deprecation reads@deprecated since 0.1.0, removed in 2.0.0: the member keeps working until 2.0.0.- Only the latest major is supported, and fixes ship on its latest minor.
- The rename. Before anything was published, the Light modules were renamed from
MageRex_*toMrx_*, with the namespaceMrx\, the packagesmrx/module-<module>, the CLI prefixmrx:and the composer keyextra.mrx-light-api. MageRex_DemoData, MageRex_Branding, the child themeMageRex/studioandmagerex:demo:seedkept their names. Upgrading lists every old name.
The version rules
The surface snapshot holds one fact per line: every @api type and its declared members, and every pool, key, container, handle, core code, Light route, ACL id, form, token, icon, markup hook, JS module export, #mrx-config key and event. SurfaceTest compares it with the code.
- Breaking: a removed or changed line, with one exception: a method on a class (not an interface) whose old parameter list is a prefix of the new one, where every new parameter is optional. Also breaking: a new method on an interface, and a new abstract method.
- Additive: every other new line, and marking a line deprecated.
- Once
releasedis set:- a breaking change needs a higher major than
released; whilereleasedis 0.x, the next minor is enough (0.2.0 after 0.1.0) - an additive change needs a higher major.minor; a patch bump alone fails
- a patch bump is valid only when the surface didn't change
VERSIONequal toreleasedallows no change, andVERSIONbelowreleasedalways fails
- a breaking change needs a higher major than
- Only members declared in Light code count. Members Light classes inherit from Magento, such as
Template::toHtml(), never do, so a Mage-OS release can't force a Light major through them. Parameter names are part of the promise, because callers may use named arguments. - Core codes, Light routes, ACL ids and form codes are what other modules point at, so they are never renamed or removed before the next major. The values of core items are tier 2.
The rules as a decision, for a release of Light:
What is contract
| Level | What | Promise |
|---|---|---|
| Tier 1, contract | @api PHP (declared members); catalogue pools, keys, containers and handles; core codes in referenced pools, Light routes, ACL ids and forms; tokens, icons, markup hooks, JS modules, events and #mrx-config keys; the editor save response keys success, message, entity_id and ext; extra.mrx-light-api, CLI command names, and ExtensionApi with VERSION and satisfies(); the compatibility gate: who declares a range, who needs a range guard, the statuses, and what a hidden module loses and keeps | In surface.json, under the version rules above. A gate rule that hides more is a major |
| Tier 2, works but not versioned | Changing or removing core items by key (Changing Light for one shop); layout referenceBlock, move and remove on core blocks, and their arguments; template overrides; RequireJS mixins on Mrx_*/js/*; CSS on mrx-* classes; plugins on @api classes; source strings | Retest after every minor. Changelog lists "Changed defaults" |
| Tier 3, not supported | <preference> or <plugin> on non-@api Light classes (the doctor reports private_api); a RequireJS map that replaces a Light module; edits under app/code/Mrx | May break in any release |
| Internal | Non-@api classes, and block methods called from core templates; the JSON of Light's own controllers and its cache entries; markup outside the documented classes; dev/mrx_light/* flags and tests/** helpers; the compatibility map, its cache entry and the wording of its messages and banner | Not for modules |
| Stored state | admin_user.extra keys mrx_mode, mrx_apps, mrx_table_columns, mrx_theme and mrx_home_setup_hidden | Internal. Readers accept older shapes, and a stored code is never renamed without a data patch |
What a release may change
| Topic | Rule |
|---|---|
| Interfaces | An @api interface doesn't change within a major. New behaviour ships as a new optional interface, checked with instanceof, the way FilterableProviderInterface, ChannelAwareCounterInterface, DeclaresFieldsInterface and AppliesToEntityInterface already work |
| Classes | An @api class may gain public methods and trailing optional parameters. An @api abstract class never gains an abstract method |
| Feature detection | Feature-detect with interface_exists() or instanceof. satisfies() is for range guards, the doctor and the compatibility gate |
ExtensionApi | ExtensionApi, VERSION and satisfies() keep their name and meaning in every major, because every range guard calls them |
| Packages | A package's major.minor equals the API's major.minor. Patch releases are package-only, and VERSION stays x.y.0 |
| Deprecation | The tag reads @deprecated since <x.y.z>, removed in <X.0.0>, use <replacement>. The surface marks the line, and the doctor warns about use (deprecated_use). Light never raises runtime deprecation notices. A deprecated item stays for at least one minor release and six months |
| Doctor rules | A rule added after 1.0.0 reports as a warning until the next major. An existing rule's level never rises in a minor |
| Core modules | Disabling one of the 14 core modules is not supported. MageRex_Branding, MageRex_DemoData and the -hyva modules are not core and may be disabled |
Tested platforms
Light 0.1, the public beta, was tested on these versions, read from composer.lock on 2026-09-27. A later platform release is absorbed in a patch or a minor unless it forces a change to an @api signature.
| Component | Version | Note |
|---|---|---|
Mage-OS (mage-os/product-community-edition, mage-os/framework) | 3.5.0 | Magento Open Source 2.4.9 |
| PHP | 8.5 | |
disrex/module-ai | 1.3.0 | A fixed dependency: Light requires ^1.3 |
composer/semver | 3.5.0 | The only dependency of ExtensionApi |
| Symfony | 7.4.19 | |
Hyvä (hyva-themes/magento2-theme-module, magento2-default-theme) | 1.5.2 | Storefront only, through the -hyva modules |
hyva-themes/magento2-compat-module-fallback | 1.1.4 | Storefront only |
mage-os/module-rma | 2.4.1 | For Mrx_Returns, which a shop installs only when it wants returns |
mollie/magento2 | 3.1.4 | For Mrx_PaymentsMollie |
Before each Light release and each platform upgrade, tests/compat/pattern-b.sh runs with the newest Mage-OS release in PLATFORM_VERSION (Testing). A failure goes into the watch list below with its date.
Known future breaking changes
| # | Trigger | What breaks | What absorbs it | Source |
|---|---|---|---|---|
| 1 | The 1.0.0 release after the beta: 0.x becomes 1.0.0 | Every ^0.4 range, because a caret on 0.x never matches 1.0.0 (a beta minor such as 0.5.0 leaves it out of range too). Every module that contributes to Light without a range: from 1.0.0 it counts as out of range | Every range in this repository moves to ^1.0 at the 1.0.0 release. api_undeclared warns throughout 0.x | Owner decisions, 2026-09-26 and 2026-09-28 |
| 2 | Light ships as Composer packages | The path under app/code, the psr-0 route class_exists() relies on, require against extra | The range guard, the runtime surface in the doctor (private_api), the package rule, the bridge rules | Unverified, no date |
| 3 | Mage-OS feature majors, about April and October | 3.0 retired Laminas MVC, moved TinyMCE to HugeRTE, Zend_Cache to Symfony Cache and Symfony 6.4 to 7.4. That hits AbstractPage, Template blocks and cache types | The declared-members surface and a PLATFORM_VERSION run | composer.lock on 2026-09-26; the release cadence is unverified |
| 4 | The Magento Open Source 2.4.x line | Upstream changes that Mage-OS merges | The same as 3 | vendor/mage-os/framework/composer.json on 2026-09-26; the upstream roadmap is unverified |
| 5 | PHP 9 removes what PHP 8.5 deprecates | Code that still uses a deprecated feature | Unit runs on the newest PHP with deprecations reported; fixes ship as a patch | Unverified |
| 6 | Mage-OS renames the admin images the neutral brand uses | The neutral logo and favicon | The brand falls back and logs the missing file; the white-label spec checks the files | vendor/mage-os/module-backend layouts, 2026-09-26 |
| 7 | The next Hyvä major | The pilot's storefront form and the -hyva modules | The pilot's storefront browser test | composer.lock: Hyvä 1.5.2, 2026-09-26 |
| 8 | disrex/module-ai 2.x | LlmClientInterface and BudgetExceededException on every shop | Mrx_Ai's range ^1.3 keeps 2.x out until it is tested | composer.lock: 1.3.0, 2026-09-26 |
| 9 | jQuery, RequireJS or Knockout changes in the stock admin | Light's admin JS and the js surface | This would be a Light major | Unverified |
| 10 | Composer changes the autoload.files order | The pattern-B range guard | CompatGuardTest and the pattern-B job | vendor/composer/autoload_files.php, 2026-09-26 |
| 11 | Magento changes its commit callbacks | The rule that side effects run in <entity>_save_commit_after | The form-card pitfalls, retested per platform | vendor/mage-os/framework/Model/ExecuteCommitCallbacks.php, 2026-09-26 |
| 12 | Light 2.0 | Modules that declare ^1.x: the gate hides them, and the range guard leaves those with Light PHP unregistered. Modules with Light PHP and no range guard: a changed signature is a compile error that no gate can stop | The range guard, and the api_constraint error "no range guard" throughout 1.x | Owner requirement L20, 2026-09-26 |
Your module's own identifiers
Your module's identifiers are stored or bookmarked, so a minor release of your module must never rename them:
| Identifier | Where it is kept |
|---|---|
App code (the apps key, usually the module name) | admin_user.extra mrx_apps (pins and hidden apps) |
| Nav item keys | admin_user.extra mrx_apps (pins), the badge cache, other modules' parent and badge keys |
| IndexTable provider codes and column keys | admin_user.extra mrx_table_columns (chosen columns), URLs |
| Badge provider codes | The badge cache |
| Home to-do keys | Other modules that change or remove them |
| Settings page codes | URLs (light/settings/app/page/<code>), ConfigSections |
light/<controller> paths | URLs and bookmarks |
Moving from pattern A to pattern B keeps every identifier and route path. The base module's release then declares "conflict": {"<vendor>/module-<name>-light": "*"}, and the doctor's light_collision flags a bridge left installed.
Declaring the range
extra.mrx-light-apiin yourcomposer.jsonis the range of Light API versions your module supports:^0.4during the beta (^1.0from the later 1.0.0 release).ExtensionApi::satisfies('<range>')checks it againstVERSIONwith composer/semver.- A module with Light PHP repeats the range in the range guard of its
registration.php, so it never registers on a Light outside the range. The pilot's guard:
if (class_exists(\Mrx\Light\Api\ExtensionApi::class) && \Mrx\Light\Api\ExtensionApi::satisfies('^0.4')) {ExtensionApikeeps its name, its namespace and the meaning ofVERSIONandsatisfies()in every major, because every range guard checks it before Magento boots.- The constraint rule. A module may list
mrx/module-lightunderrequire, because Magento's dependency check reads the package names to refuse amodule:disablethat another module needs. The range there must equalextra.mrx-light-api, and the doctor'sapi_constraintreports a difference. Composer ignores acomposer.jsonunderapp/code, so nothing resolves that range until the Light packages are published; then Composer checks it too. Pattern B never requires Light: once the packages are published, it puts the inverse range underconflict, for example<1.0 || >=2.0for^1.0.
Compatibility gate
Light compares every module's declared range with VERSION, and hides the Light parts of a module that doesn't fit. Two mechanisms act on the range: the range guard in registration.php keeps a module's PHP from loading, and the gate hides what a registered module adds to Light. Both need the other.
Who declares a range
Every module that contributes to Light: one with a row in a catalogued pool (an addition, a change or a null), a light/<controller> route, or an adminhtml layout file on a Light handle (light_*, mrx_*) or with a referenceContainer named mrx.*. That covers bridges, pattern-B modules, theme and brand modules, third-party modules and our own Disrex and MageRex modules.
A module that only uses Light declares one too: one whose PHP, templates or XML name a Light class, such as a storefront observer that calls BadgeCacheInterface::invalidate(), or one that observes an mrx_* event.
Light core is exempt: the 14 modules of Surface::CORE_MODULES, Mrx_{Light,Apps,Catalog,Content,Customers,Discounts,Documents,Home,Locations,Orders,Returns,Settings,Themes,Ai}. They ship with VERSION itself. Mrx_Returns is the one core module a shop may leave out: magerex/distribution-nl only suggests mrx/module-returns, because it brings mage-os/module-rma. Every other module is gated: MageRex_Branding, MageRex_DemoData, the -hyva modules, the packs in packages/ (Mrx_CountryNl, Mrx_PaymentsMollie, Mrx_PaymentsPaynl), Disrex modules and any new Mrx_* module included.
Who needs a range guard
Every module whose .php or .phtml files, outside registration.php, Test/ and tests/, name a Light class. Only the guard keeps its classes, observers, plugins, cron jobs and commands from loading on a Light it doesn't fit: the gate hides pool entries, layout and light routes, nothing more. Without the guard, the doctor reports the api_constraint error "no range guard". A pattern-B base module, a theme module and a brand package have no Light PHP and need no guard.
Where the range goes
- A packaged module declares it in its package's
composer.json. A module inapp/codewithout a package keeps a smallcomposer.jsonin its own folder withnameandextra. Light takes the nearestcomposer.jsonat the module's path or above it inside the same package, sosrc/andLightCompat/share the package root's range. - Declare
^<major>.<minor>of the lowest API you need, or several such parts joined by||, for example^1.0 || ^2.0. Any other form gets the warningapi_range_form:*and>=1.0match every future major, so the gate can't protect you, and~1.0.0,1.0.*and1.0.0exclude 1.1.0, so the next minor would hide your module. During the beta the range is^0.4.
"extra": {
"mrx-light-api": "^0.4"
},Acme_LightFuture is the committed proof of a module that doesn't fit. It declares a range no Light has yet, and its tests check what the gate hides:
"mrx-light-api": "^9.0"Statuses
How one module moves between the statuses, as its range, its registration and VERSION change:
| Status | Meaning | What Light does |
|---|---|---|
ok | The range is satisfied | Nothing |
out_of_range | Registered and enabled, and satisfies() is false (a malformed range and a branch name such as dev-main included) | Hides its Light parts |
skipped | It declares a range and its range guard left it unregistered | Nothing of it loaded; Light reports it |
undeclared | It contributes to Light or uses it, and declares no range | During 0.x it stays loaded and the doctor warns. From the later 1.0.0 release the gate hides one that contributes, like an out_of_range module |
core | One of the 14 core modules | Always compatible |
A hidden module loses:
- its pool entries: nav items and counters, apps, claims and pins, settings pages, cards and emails, header actions, palette entries, Home to-dos and setup steps, form extensions, list providers and columns, order actions, timelines and totals, redirects, themes, brands and icons, and its resources in role areas
- its adminhtml layout files on a Light handle or with an
mrx.*reference, so its cards, widgets and JS hooks - its
light/<controller>routes, which answer 404
A hidden module keeps:
- its tier-2 changes to other modules' items, core items included: a core nav item it relabelled keeps the new label, because DI has already merged it. The doctor lists every such row in the module's
api_constrainterror - its tier-3 use: plugins, preferences and RequireJS mixins
- its own PHP outside the pools while it is registered: observers, plugins, cron jobs, REST and storefront code, and console commands. Only the range guard stops those
- its stock admin screens, so a pattern-B base module still works in advanced mode
Light references only on Light handles. The gate drops whole layout files. A default.xml that changes a stock page and also references mrx.home.widgets would lose its stock change too while the module is hidden. So put Light references only in layout files on light_* or mrx_* handles; the doctor warns about any other file with mixed_layout.
Developer, production and default mode

Every mode hides the same parts, and nothing blocks: no exception, and every page keeps working.
- Developer mode adds a red banner above the title of every Light page. It lists each hidden or skipped module with its range, the Light API version and the fix. Stock pages in advanced mode and the sign-in page show no banner.
- Production and default mode log one line per hidden or skipped module when the map is built, so once per cache lifetime, and show nothing: no admin notice and no email. The doctor and the
setup:upgradeoutput give the details. - After you fix a module, run
bin/magento cache:clean config layout. The map lives in the config cache and the merged layout in the layout cache; cleaningconfigalone leaves the stale layout.
Compatibility gate messages lists every message with its cause and fix.
No range, pre-releases and dev versions
- No range. During 0.x an
undeclaredmodule stays loaded: the doctor warns withapi_undeclared, andsetup:upgradeprints a line. From the later 1.0.0 release anundeclaredmodule that contributes counts as out of range, andapi_undeclaredis an error. - Pre-releases.
VERSIONis alwaysx.y.0. A pre-release package of Light, such as2.0.0-beta1, carries theVERSIONit is heading for, so a module declaring^2.0loads on it and one declaring^1.0is hidden. - Your module's own version plays no part, stable, pre-release or
dev-*. Only its declared range does.satisfies()ignores a stability flag such as@dev, and a branch name such asdev-mainnever matches; both getapi_range_form.
How releases move a module
- A major is the only release that breaks, and it moves every
^1.xmodule out of range. The range guard leaves a module with Light PHP unregistered, so an old signature never reaches PHP, and the gate reports it asskipped. A registered module without Light PHP is hidden. A module with Light PHP and no guard is the one case the gate can't save: a changed signature is a compile error for the whole shop. - A minor only adds, so
^1.0modules stay in range on 1.1. A module that needs a 1.1 feature declares^1.1and is hidden on 1.0. During 0.x this doesn't hold: a caret on 0.x pins the minor. - A patch never changes the gate's decision.
- A deprecation keeps the member until the next major. A module that still uses it keeps loading throughout 1.x and gets
deprecated_use. After it moves off the deprecated member and is tested on 2.0, it widens its range to^1.0 || ^2.0, inextraand in the guard.
Composer
Once mrx/module-light is published, a pattern-A bridge requires the same range as its extra, and a pattern-B module puts the inverse range under conflict, so Composer refuses the combination before anything installs. A package's major.minor equals the API's, so one range string serves both. Until then Composer can't check it, and extra is the only source.
Upgrading the module behind your bridge
When the module your bridge serves releases a new version:
- Test the bridge against it, then widen the
requirerange on that module in yourcomposer.json. - Run
bin/magento mrx:light:doctor --module=<your bridge>. It reportsrequire_rangewhen the installed version is outside yourrequirerange (it skipsdev-*versions). - When the two versions need different code, branch inside
Model/Backend, where every call into the other module lives, withComposer\InstalledVersions::satisfies(new VersionParser(), '<package>', '<range>'). - Test in CI at the lowest and the highest version your range allows.
Last updated on