Addon Manifest Migration
The v24 addon API changeset: what every addon maintainer has to change to keep their addon loading.
Light Store v24 replaces addon discovery-by-reflection with a manifest. Every addon now
declares itself in its own composer.json, and the store wires the addon into Laravel by
convention instead of making each addon ship a service provider that does it by hand.
This is a breaking change
An addon built for v23 or earlier will not be discovered at all on v24. Every addon needs the migration below.
Why it changed
Discovery used to scan every top-level .php file in an addon folder, reflect on each class, and
look for a StoreAddon subclass. The identifier, name and version lived as protected properties on
that class, so the store had to load and instantiate PHP before it could tell you what an addon
even was. A broken addon took the panel down with it, a disabled addon could not name itself, and
nothing could be validated before install.
The manifest is read as plain JSON, so the store now knows an addon's identity, its version requirements and its dependencies without executing any of its code.
v23 vs v24
| v23 | v24 |
|---|---|
Identity in protected string $identifier etc. | Identity in composer.json under extra.lightstore |
| Main class found by reflection over top-level files | Main class named by extra.lightstore.addon |
Classes autoloaded from a hardcoded Addon\{Folder} guess | Classes autoloaded from autoload.psr-4 |
| Each addon ships a provider that loads routes/views/lang | Loaded by convention, provider optional |
Routes registered inside the provider's boot() | routes/web.php, grouped under web for you |
Name and description translated with __() | Plain strings in the manifest, no longer translatable |
config('settings.general.site_name') in Blade | $generalSettings->site_name |
Install by unzipping, then php artisan migrate by hand | Admin panel, Addons then Install addon |
Migrating an addon
Add a composer.json
Create it at the root of your addon folder, next to the main class. This file is now the single source of truth for your addon's identity.
{
"name": "nortexdev/lightstore-points",
"description": "Reward users with loyalty points on every purchase",
"version": "2.0.0",
"type": "library",
"license": "MIT",
"homepage": "https://lightstore.nortex.dev",
"authors": [{ "name": "NorteX" }],
"require": {
"php": ">=8.4.0",
"nortexdev/lightstore": ">=24.0"
},
"autoload": {
"psr-4": {
"Addon\\PointsAddon\\": ""
}
},
"extra": {
"laravel": {
"providers": ["Addon\\PointsAddon\\Providers\\PointsServiceProvider"]
},
"lightstore": {
"identifier": "points",
"name": "Points",
"addon": "Addon\\PointsAddon\\PointsAddon"
}
}
}The identifier is a data key
extra.lightstore.identifier is the primary key in the addon_settings table, and the namespace
for your views, translations and config. Use exactly the string your v23 addon returned from
getIdentifier(). Changing it orphans every stored setting, so treat it as a data migration
rather than a rename.
Strip the metadata off the main class
All six identity properties are gone from StoreAddon. Delete them, along with any getName() or
getDescription() override. Those values now come from the manifest.
class PointsAddon extends StoreAddon
{
protected string $identifier = "points";
protected string $version = "1.1.0";
protected string $author = "NorteX";
public function getName(): string
{
return __("points::addon.Points");
}
public function getDescription(): string
{
return __("points::addon.Reward users with loyalty points on every purchase");
}
public function getConfig(array $values = []): array { /* ... */ }
}class PointsAddon extends StoreAddon
{
public function getConfig(array $values = []): array { /* ... */ }
}The properties that no longer exist are $identifier, $name, $version, $description,
$author and $url.
The getters themselves stay and keep working. getIdentifier(), getName(), getVersion(),
getDescription(), getAuthor() and getUrl() all read through to the manifest. Three members
are new: getManifest(), getPath() for the absolute path to your addon folder, and
setManifest(), which the store calls for you right after it constructs your class.
Every hook method is unchanged. getConfig(), boot(), enabled(), disabled(), the
Filament getters, the cart and product view hooks, getProfileTabs(), getDataExport() and
getAccountDeletionSummary() all keep their exact v23 signatures.
Delete the service provider boilerplate
The store now loads routes, views, translations, migrations and config for you. For most addons the
whole service provider goes away. Drop it, and leave extra.laravel.providers out of the manifest
entirely.
class KnowledgebaseServiceProvider extends ServiceProvider
{
public function boot(): void
{
$routesPath = __DIR__ . "/../routes/web.php";
if (file_exists($routesPath)) {
Route::middleware("web")->group(fn() => require $routesPath);
}
$viewsPath = __DIR__ . "/../resources/views";
if (is_dir($viewsPath)) {
$this->loadViewsFrom($viewsPath, "knowledgebase");
}
$langPath = __DIR__ . "/../lang";
if (is_dir($langPath)) {
$this->loadTranslationsFrom($langPath, "knowledgebase");
}
$migrationsPath = __DIR__ . "/../database/migrations";
if (is_dir($migrationsPath)) {
$this->loadMigrationsFrom($migrationsPath);
}
}
}Keep a provider only for the things the conventions do not cover, such as event listeners, container bindings, macros and policies. Once the boilerplate is gone, it usually shrinks to just that:
class PointsServiceProvider extends ServiceProvider
{
public function register(): void {}
public function boot(): void
{
Event::listen(ItemPurchased::class, GrantPointsListener::class);
}
}Move routes into routes/web.php
Routes defined inside a provider's boot() need to move to a routes/web.php file in your addon
folder. The store groups that file under the web middleware itself.
<?php
declare(strict_types=1);
use Addon\PointsAddon\Http\Controllers\PointsController;
use Illuminate\Support\Facades\Route;
Route::middleware("auth")->group(function () {
Route::get("/profile/points", [PointsController::class, "show"])->name("points.profile");
});Do not re-apply `web`
The store already wraps the file in Route::middleware("web"). Listing web again in your own
group runs the session, cookie and CSRF middleware twice.
Drop the name and description translation keys
Addon names and descriptions are no longer translatable. They are plain strings, read from
extra.lightstore.name and the manifest's top-level description. Remove those two keys from
every lang/{locale}/addon.php. Everything else in your language files is untouched.
return [
"Points" => "Points",
"Reward users with loyalty points on every purchase" => "...",
// Settings
"Earn Multiplier" => "Earn Multiplier",
];Keep a "Points" style key if your own views, navigation links or getConfig() labels still use
it. Only the two entries the admin panel used to read are obsolete.
Check your Blade against the settings change
Separately from the manifest work, v24 removed the settings-into-config() injection. Any addon
view still calling config('settings.*') now reads null.
@section('title', __('points::addon.Points') . ' - ' . config('settings.general.site_name')) {/* [!code --] */}
@section('title', __('points::addon.Points') . ' - ' . $generalSettings->site_name) {/* [!code ++] */}Every settings group is shared with all views as a variable named after its class:
$generalSettings, $captchaSettings, $footerSettings, $integrationsSettings,
$oauth2Settings, $socialSettings and $statsSettings. Outside Blade, resolve the class
directly with app(GeneralSettings::class).
The conventional layout
Lay your addon out like this and the store wires all of it up. Anything that is absent is simply skipped.
| Path | What the store does | When |
|---|---|---|
lang/ | loadTranslationsFrom(..., identifier) | Always, even when disabled |
resources/views/ | loadViewsFrom(..., identifier) | Enabled only |
database/migrations/ | loadMigrationsFrom(...) | Enabled only |
routes/web.php | Grouped under the web middleware | Enabled only |
config/config.php | mergeConfigFrom(..., identifier), so config("points.foo") | Enabled only, at register |
vendor/autoload.php | Required before your PSR-4 prefixes are registered | Always, if present |
Translations load for disabled addons too. The admin panel still offers a Settings button for a
disabled addon, and the labels and descriptions on that form come out of getConfig(), where they
are usually __() calls against your language files. Without them loaded, that form would render
raw language keys.
Manifest reference
| Key | Required | Purpose |
|---|---|---|
extra.lightstore.identifier | Yes | Settings key and view/lang/config namespace. Must match /^[a-z0-9][a-z0-9_-]*$/ |
extra.lightstore.addon | Yes | Fully qualified name of your StoreAddon subclass |
autoload.psr-4 | Yes | Namespace prefix to directory, relative to the addon root |
extra.lightstore.name | No | Display name. Falls back to a headline-cased identifier |
name | No | Composer package name. Falls back to the identifier |
description | No | Shown in the admin panel |
version | No | Shown in the admin panel. Defaults to 0.0.0 |
authors[0].name | No | Shown in the admin panel |
homepage | No | Links the addon's name in the admin panel |
require | No | Checked at install, never resolved |
extra.laravel.providers | No | Extra service providers, registered only while the addon is enabled |
autoload.psr-4 is registered against the running Composer class loader at boot. That is what
lets an addon installed after the Docker image was built autoload at all, so the prefix has to be
correct even though nobody runs composer dump-autoload on the store.
Declaring dependencies
require is validated but never resolved. The store will not download anything on your behalf.
php,ext-*andlib-*entries are ignored.nortexdev/lightstoreis treated as a core version constraint. At install the store checks it against its own version with Semver and refuses the addon if it does not satisfy, so">=24.0"is the right marker for a migrated addon.- Every other package must either already ship with Light Store, or your addon must carry its own
vendor/directory. If it does, the store requiresvendor/autoload.phpand skips the dependency check entirely.
Packaging and installing
Zip the addon folder itself, meaning the folder with composer.json at its root. Users install it
from Addons then Install addon in the admin panel, which extracts it, validates the
manifest, copies it into addons/, runs your migrations by path, enables it and seeds your default
settings. Nothing needs rebuilding or restarting.
Installing over an existing copy upgrades it in place and keeps its settings and data. The previous
copy is set aside under a dotted folder name, which discovery skips. Your addon folder name has to
match /^[A-Za-z0-9][A-Za-z0-9_-]*$/.
Scaffolding a new addon
php artisan addon:create {name} generates the whole layout above with a correct manifest,
including a nortexdev/lightstore constraint pinned to the store's current major version.
Install errors
| Message | Cause |
|---|---|
| Addon does not appear at all after uploading | No composer.json, unreadable JSON, or a missing or malformed extra.lightstore.identifier |
...does not name its main class in extra.lightstore.addon | The key is missing from the manifest |
...declares no autoload.psr-4 namespace | autoload.psr-4 is missing or empty |
...needs Light Store {constraint} and this store is {version} | Your nortexdev/lightstore constraint excludes the target store |
...requires {packages}, which this store does not ship | Ship a vendor/ directory with the addon |
Class {FQCN} was not found in the log | The PSR-4 prefix does not match the main class's real namespace or path |
Styling
Do not ship a stylesheet with your addon. Tailwind expresses breakpoint precedence purely as source
order within one @layer utilities, so a second sheet loaded after the theme hoists every utility
it repeats above every responsive variant in the theme. That breaks the whole storefront, not just
your own pages.
The store's build cannot see your markup either, since addons are kept out of the image build
context. Utilities are covered by a generated safelist that is rebuilt when addons are packaged. In
practice, stay with utilities the shipped default theme already uses, and reach for a plain
style attribute for anything genuinely exotic.