Light API 0.4.0 is a public beta: a minor release such as 0.5.0 may still break. What that means for your module
Light
Developer guide

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::VERSION is the Light API version. 0.1.0 is 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 VERSION stays x.y.0.
  • The surface snapshot, app/code/Mrx/Light/Test/Unit/Api/_files/surface.json, has a field released: the last published API version. It was null before the beta, is 0.1.0 from the beta launch and becomes 1.0.0 at the 1.0.0 release. There is no separate marker file.
  • What ^0.4 means. Composer reads a caret on a 0.x version as a pin on the minor: ^0.4 is >=0.4.0 <0.5.0. So a module on ^0.4 is 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.4 to ^1.0. From 1.0.0 the caret pins the major: ^1.0 is >=1.0.0 <2.0.0, so a module stays in range across minors.
  • @since tags say 0.1.0 on everything the beta publishes, and so does the since column 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_* to Mrx_*, with the namespace Mrx\, the packages mrx/module-<module>, the CLI prefix mrx: and the composer key extra.mrx-light-api. MageRex_DemoData, MageRex_Branding, the child theme MageRex/studio and magerex:demo:seed kept 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 released is set:
    • a breaking change needs a higher major than released; while released is 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
    • VERSION equal to released allows no change, and VERSION below released always fails
  • 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

LevelWhatPromise
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 keepsIn surface.json, under the version rules above. A gate rule that hides more is a major
Tier 2, works but not versionedChanging 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 stringsRetest 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/MrxMay break in any release
InternalNon-@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 bannerNot for modules
Stored stateadmin_user.extra keys mrx_mode, mrx_apps, mrx_table_columns, mrx_theme and mrx_home_setup_hiddenInternal. Readers accept older shapes, and a stored code is never renamed without a data patch

What a release may change

TopicRule
InterfacesAn @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
ClassesAn @api class may gain public methods and trailing optional parameters. An @api abstract class never gains an abstract method
Feature detectionFeature-detect with interface_exists() or instanceof. satisfies() is for range guards, the doctor and the compatibility gate
ExtensionApiExtensionApi, VERSION and satisfies() keep their name and meaning in every major, because every range guard calls them
PackagesA package's major.minor equals the API's major.minor. Patch releases are package-only, and VERSION stays x.y.0
DeprecationThe 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 rulesA 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 modulesDisabling 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.

ComponentVersionNote
Mage-OS (mage-os/product-community-edition, mage-os/framework)3.5.0Magento Open Source 2.4.9
PHP8.5
disrex/module-ai1.3.0A fixed dependency: Light requires ^1.3
composer/semver3.5.0The only dependency of ExtensionApi
Symfony7.4.19
Hyvä (hyva-themes/magento2-theme-module, magento2-default-theme)1.5.2Storefront only, through the -hyva modules
hyva-themes/magento2-compat-module-fallback1.1.4Storefront only
mage-os/module-rma2.4.1For Mrx_Returns, which a shop installs only when it wants returns
mollie/magento23.1.4For 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

#TriggerWhat breaksWhat absorbs itSource
1The 1.0.0 release after the beta: 0.x becomes 1.0.0Every ^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 rangeEvery range in this repository moves to ^1.0 at the 1.0.0 release. api_undeclared warns throughout 0.xOwner decisions, 2026-09-26 and 2026-09-28
2Light ships as Composer packagesThe path under app/code, the psr-0 route class_exists() relies on, require against extraThe range guard, the runtime surface in the doctor (private_api), the package rule, the bridge rulesUnverified, no date
3Mage-OS feature majors, about April and October3.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 typesThe declared-members surface and a PLATFORM_VERSION runcomposer.lock on 2026-09-26; the release cadence is unverified
4The Magento Open Source 2.4.x lineUpstream changes that Mage-OS mergesThe same as 3vendor/mage-os/framework/composer.json on 2026-09-26; the upstream roadmap is unverified
5PHP 9 removes what PHP 8.5 deprecatesCode that still uses a deprecated featureUnit runs on the newest PHP with deprecations reported; fixes ship as a patchUnverified
6Mage-OS renames the admin images the neutral brand usesThe neutral logo and faviconThe brand falls back and logs the missing file; the white-label spec checks the filesvendor/mage-os/module-backend layouts, 2026-09-26
7The next Hyvä majorThe pilot's storefront form and the -hyva modulesThe pilot's storefront browser testcomposer.lock: Hyvä 1.5.2, 2026-09-26
8disrex/module-ai 2.xLlmClientInterface and BudgetExceededException on every shopMrx_Ai's range ^1.3 keeps 2.x out until it is testedcomposer.lock: 1.3.0, 2026-09-26
9jQuery, RequireJS or Knockout changes in the stock adminLight's admin JS and the js surfaceThis would be a Light majorUnverified
10Composer changes the autoload.files orderThe pattern-B range guardCompatGuardTest and the pattern-B jobvendor/composer/autoload_files.php, 2026-09-26
11Magento changes its commit callbacksThe rule that side effects run in <entity>_save_commit_afterThe form-card pitfalls, retested per platformvendor/mage-os/framework/Model/ExecuteCommitCallbacks.php, 2026-09-26
12Light 2.0Modules 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 stopThe range guard, and the api_constraint error "no range guard" throughout 1.xOwner 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:

IdentifierWhere it is kept
App code (the apps key, usually the module name)admin_user.extra mrx_apps (pins and hidden apps)
Nav item keysadmin_user.extra mrx_apps (pins), the badge cache, other modules' parent and badge keys
IndexTable provider codes and column keysadmin_user.extra mrx_table_columns (chosen columns), URLs
Badge provider codesThe badge cache
Home to-do keysOther modules that change or remove them
Settings page codesURLs (light/settings/app/page/<code>), ConfigSections
light/<controller> pathsURLs 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-api in your composer.json is the range of Light API versions your module supports: ^0.4 during the beta (^1.0 from the later 1.0.0 release). ExtensionApi::satisfies('<range>') checks it against VERSION with 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:
app/code/Disrex/RequestAnAccountLight/registration.php
if (class_exists(\Mrx\Light\Api\ExtensionApi::class) && \Mrx\Light\Api\ExtensionApi::satisfies('^0.4')) {
  • ExtensionApi keeps its name, its namespace and the meaning of VERSION and satisfies() in every major, because every range guard checks it before Magento boots.
  • The constraint rule. A module may list mrx/module-light under require, because Magento's dependency check reads the package names to refuse a module:disable that another module needs. The range there must equal extra.mrx-light-api, and the doctor's api_constraint reports a difference. Composer ignores a composer.json under app/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 under conflict, for example <1.0 || >=2.0 for ^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 in app/code without a package keeps a small composer.json in its own folder with name and extra. Light takes the nearest composer.json at the module's path or above it inside the same package, so src/ and LightCompat/ 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 warning api_range_form: * and >=1.0 match every future major, so the gate can't protect you, and ~1.0.0, 1.0.* and 1.0.0 exclude 1.1.0, so the next minor would hide your module. During the beta the range is ^0.4.
app/code/Disrex/RequestAnAccountLight/composer.json
"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:

app/code/Acme/LightFuture/composer.json
"mrx-light-api": "^9.0"

Statuses

How one module moves between the statuses, as its range, its registration and VERSION change:

StatusMeaningWhat Light does
okThe range is satisfiedNothing
out_of_rangeRegistered and enabled, and satisfies() is false (a malformed range and a branch name such as dev-main included)Hides its Light parts
skippedIt declares a range and its range guard left it unregisteredNothing of it loaded; Light reports it
undeclaredIt contributes to Light or uses it, and declares no rangeDuring 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
coreOne of the 14 core modulesAlways 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_constraint error
  • 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

The red compatibility banner on a Light page in developer mode, with the hidden module, its range and the fix

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:upgrade output 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; cleaning config alone 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 undeclared module stays loaded: the doctor warns with api_undeclared, and setup:upgrade prints a line. From the later 1.0.0 release an undeclared module that contributes counts as out of range, and api_undeclared is an error.
  • Pre-releases. VERSION is always x.y.0. A pre-release package of Light, such as 2.0.0-beta1, carries the VERSION it is heading for, so a module declaring ^2.0 loads on it and one declaring ^1.0 is 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 as dev-main never matches; both get api_range_form.

How releases move a module

  • A major is the only release that breaks, and it moves every ^1.x module 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 as skipped. 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.0 modules stay in range on 1.1. A module that needs a 1.1 feature declares ^1.1 and 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, in extra and 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:

  1. Test the bridge against it, then widen the require range on that module in your composer.json.
  2. Run bin/magento mrx:light:doctor --module=<your bridge>. It reports require_range when the installed version is outside your require range (it skips dev-* versions).
  3. When the two versions need different code, branch inside Model/Backend, where every call into the other module lives, with Composer\InstalledVersions::satisfies(new VersionParser(), '<package>', '<range>').
  4. Test in CI at the lowest and the highest version your range allows.

Last updated on

On this page