Appearance
Enabling & Disabling Modules
Introduction
The Modules screen is kitloom's answer to the WordPress Plugins screen. It lists every installed module with its name, description, version, package and admin packages, shows whether it is Active or Inactive, and lets an administrator Deactivate and Activate it.
| Panel | Where |
|---|---|
| Filament | Modules → Installed Modules (/admin/modules), between Appearance and Users |
| Nova | Modules → Installed Modules (/nova/modules) |
The screen is guarded by the activate_plugins permission, which only administrators have by default. It switches modules that are already installed; installing one is a deployment — see Installing Modules.
How it works, per module
Every module on the screen has a How it works link: the README of its domain package, rendered as a page of the admin — /admin/modules/kitloom/redirects in Filament, /nova/modules/kitloom/redirects in Nova. The same page is the last item of the module's own menu section, after its screens and Settings. Any signed-in user of the admin may open it: it describes the module, not the site's data. A package without a README has no page and no link.
That is why a module's README is written for the person who configures the module: what it does, where it keeps things, which settings and routes it has.
A site whose menu should hold only its own screens sets KITLOOM_MODULES_MENU_HELP=false (kitloom-modules.menu_help). Then:
- How it works is left out of every module's menu section, in Filament and in Nova — no module needs a change for it;
- the Modules screen still links each module to its page;
- the pages themselves stay, and their addresses open as before.
The value is read each time the menu is built, so a config changed at runtime applies on the next request.
Hooking it in
Deactivation works by not registering the module's service providers. Packages are not loaded yet at that point, so the site hands this over once, in bootstrap/app.php:
php
use Kitloom\Modules\Modules;
$app = Application::configure(basePath: dirname(__DIR__))
->withRouting(/* ... */)
->withMiddleware(/* ... */)
->withExceptions(/* ... */)
->create();
// Modules deactivated on the Modules screen are not loaded (kitloom/modules).
Modules::bootstrap($app);
return $app;Modules::bootstrap() swaps Laravel's package manifest for one that leaves deactivated modules out, right before providers are registered, and checks the config and route caches (see After a switch). Both skeletons already contain this line. Without it, the Modules screen says Switching modules has no effect yet and deactivating changes nothing.
What deactivating does
A deactivated module is not loaded at all from the next request on:
- its service providers and the providers of its admin packages are not registered, nor are the packages that depend on it;
- its routes, REST routes, middleware, page parts, admin sections, settings pages, permissions and tools for AI agents disappear;
- its tables, options and meta stay in the database — activating it brings everything back.
The screen reloads itself after a switch. Facade aliases of a deactivated module are still registered (Laravel registers aliases one step earlier), but there is nothing behind them.
Dependencies
Modules depend on each other the way their Composer packages do, and the screen follows the same graph:
Deactivating a module also unloads every package that depends on it. That is why a module cannot be deactivated while something other than its own admin packages depends on it — another module, the active theme or a core package. The row says Required by and names them, and the attempt is refused:
The Payments module cannot be deactivated: Shop depend on it.
A theme that is installed but not active does not prevent it: it goes down with the module, and cannot be activated on Appearance → Themes until the module is back.
Activating a module is refused while a module it depends on is deactivated. The row says Needs:
The Shop module needs Payments: activate them first.
Core packages —
site,wp-schema,settings,modulesand the others listed in Overview — are not modules and cannot be deactivated.Roles and permissions (
kitloom/permissions) are deliberately not on the screen: switching them off would open the whole admin to everyone. They are installed and removed through Composer only.
After a switch: caches, workers, Octane
A switch writes two rows — the list of deactivated modules and the time of the switch — and takes effect from the next request. Long-running processes and Laravel's caches need a moment more, and kitloom takes care of them.
Caches. Laravel's config and route caches, and Filament's component cache, are built by php artisan optimize for the set of modules installed at the time. After a switch they describe the old set: a module switched on would have no config and no routes, a module switched off would leave routes that fail. What happens depends on KITLOOM_MODULES_CACHES:
| Mode | Caches built for another set of modules | Rebuilt after the switch | Pick it when |
|---|---|---|---|
auto (default) | not used | yes, on the server where the switch was made | one server, and whoever runs the admin also deploys — the way WordPress sites are run |
deploy | not used | no; php artisan optimize on the next deploy brings them back | several servers deployed from CI |
manual | used as they are | no | you run php artisan optimize yourself after every switch |
- The boot guard (
autoanddeploy). Every cachephp artisan optimizebuilds gets a small mark next to it (config.modules.jsonbesideconfig.php, and so on) naming the modules that were off while it was built. At the start of every request each server compares those marks with the modules off now and does not use a cache built for another set: routes are read from the route files, Filament finds its pages and resources itself, and the cached config gets the defaults of a newly activated module added (values that module reads from.envapply once the config cache is rebuilt). Nothing is deleted. On several servers sharing one database, each notices the switch by itself. Without its caches a server is a little slower, never wrong. Switching back to the set a cache was built for makes it good again. A cache without a mark (built by an older kitloom, copied without its mark) is compared with the time of the last switch instead. - Caches built without the database. A build step that cannot reach the database — Laravel Cloud's build commands, a Docker image, a CI artifact — sees every module on and builds for that. While every module is on, its caches are used; while one is off, they are skipped, and every deploy builds them the same way again. To keep the caches with a module off, run
php artisan optimizewhere the database can be reached. - The rebuild (
auto). Right after the response, so the administrator does not wait, the server where the switch was made rebuilds the caches it has — only those: a site that never cached anything gets no caches from a switch. A server whosebootstrap/cachethe web server cannot write skips the rebuild without an error; a rebuild that fails is reported to the error log and the server runs without that cache. A switch made from the console (php artisan tinker, a queued job) rebuilds nothing. manual. kitloom leaves the caches alone: after a switch the site behaves as Laravel does with stale caches untilphp artisan optimizeis run.- Queue workers and Horizon, in every mode, are told to restart, as
php artisan queue:restartdoes: each finishes its current job and comes back with the new set of modules. A running process cannot unload a module, so until then it works with the old set. - Octane, in every mode: on the server where the switch was made, the Octane workers reload after the response (in
auto, after the rebuild, so they boot from the new caches). Other servers running Octane keep the old set untilphp artisan octane:reloador the next deploy.
What the screen says. In auto, nothing. In deploy, when this server has caches built for another set of modules, that it runs without them until php artisan optimize is run on every server where it can reach the database. In manual, a warning naming the caches that are out of date and still in use; before any switch, a note that caches are in use and php artisan optimize must follow a switch.
KITLOOM_MODULES_CACHES is read before the providers are registered: set it in .env or in the application's config/kitloom-modules.php, not at runtime. An unknown value counts as auto.
Where the state lives
Two rows of the options table:
| Option | Example value | What |
|---|---|---|
kitloom_inactive_modules | ["kitloom/internal-links","kitloom/search-replace"] | The packages switched off on the Modules screen, as a JSON list |
kitloom_modules_changed_at | 1791820800 | Unix time of the last switch; caches without a mark that were built before it are not used, except in manual mode |
It is intentionally not WordPress's active_plugins — on a live WordPress database that option belongs to WordPress. Because the list holds deactivated modules, a module deployed tomorrow is active the moment it arrives; a name left in the list by a module that is no longer installed is ignored.
The rows are read before the database service exists, so kitloom/modules opens its own connection from your database config. It does not close it: the connection is handed over to the database service, so a request connects to the database once. It is closed only when the read fails. When the table does not exist yet — a first deploy, package:discover — nothing is considered deactivated.
Configuration
config/kitloom-modules.php; override the keys in the application's own config file of that name.
| Key | Environment | Default | What |
|---|---|---|---|
option | — | kitloom_inactive_modules | Where the list of deactivated modules is kept |
changed_option | — | kitloom_modules_changed_at | Where the time of the last switch is kept |
caches | KITLOOM_MODULES_CACHES | auto | auto, deploy or manual, see above |
menu_help | KITLOOM_MODULES_MENU_HELP | true | Whether every module's menu section ends with How it works, see above |
filament.register, nova.register | — | true | Whether the Modules screen and the How it works pages are added to the admin |
Events
A switch dispatches Kitloom\Modules\Events\ModuleActivated or Kitloom\Modules\Events\ModuleDeactivated, with the package and the actor who made it (null from code). They come from the request that made the switch, which keeps running with the old set of modules: a listener reacts to the switch, not to the module being there.
php
use Illuminate\Support\Facades\Event;
use Kitloom\Modules\Events\ModuleDeactivated;
Event::listen(function (ModuleDeactivated $event): void {
logger()->info("{$event->package} was deactivated", ['by' => $event->actor?->getAuthIdentifier()]);
});Programmatic use
The same rules are available in code through Kitloom\Modules\ModuleCatalog:
php
use Kitloom\Modules\ModuleCatalog;
$catalog = app(ModuleCatalog::class);
foreach ($catalog->all() as $module) {
echo $module->name, $module->active ? ' (active)' : '';
}
$catalog->deactivate('kitloom/internal-links', $user); // throws when something depends on it
$catalog->activate('kitloom/internal-links', $user); // throws when a module it needs is offBoth throw an InvalidArgumentException with the same message the screen shows. A Module carries package, name, description, version, active, admins (its admin packages), blockers and missing, and answers canDeactivate() and canActivate().
Caveats
- Direct use from site code. If your own application code imports classes of a module, deactivating the module can break that code. Talk to modules through registries, contracts and container tags where you can.
- Third-party packages keep loading. Only the module and the packages that depend on it are left out. The third-party packages it requires —
laravel/horizonfor the Horizon module, for example — are still discovered, and their own service providers still register. The module's classes stay autoloadable too.