coolsam/modules
Filament Modules v5 wraps nwidart/laravel-modules to turn each Laravel module into an autonomous Filament plugin with its own resources, pages, widgets, and clusters. Register ModulesPlugin once and it auto-discovers modules. Supports Laravel 11–13 and Filament 4–5.
1. Install the two-layer stack in the right order. coolsam/modules is a wrapper, not a standalone package — it will pull in nwidart/laravel-modules (^11 on Laravel 11, ^12 on Laravel 12, ^13 on Laravel 13), but you must configure that package before the wrapper does anything useful.
composer require coolsam/modules
2. Wire up module autoloading in composer.json — this is the step people skip, and everything downstream fails without it:
"extra": {
"merge-plugin": {
"include": ["Modules/*/composer.json"]
}
}
Then composer dump-autoload. Confirm config/modules.php exists and that the Modules namespace maps where you expect (php artisan module:list).
3. Run the installer and register the plugin.
php artisan modules:install
# or just: php artisan vendor:publish --tag="modules-config"
// app/Providers/Filament/AdminPanelProvider.php
use Coolsam\Modules\ModulesPlugin;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->plugin(ModulesPlugin::make());
}
That single registration is the whole point of the package: it discovers every module's generated *Plugin class and mounts it, so you never touch AdminPanelProvider again when a new module appears.
4. Where to look first.
config/filament-modules.php — the mode key is the single most consequential setting. Read the ConfigMode enum before you build anything.app/Filament/<Module>Plugin.php — this is the class ModulesPlugin auto-discovers. If a module's resources don't show up, this file and its register()/boot() bodies are your first suspect.5. First use case — a 10-minute vertical slice:
php artisan module:make Blog
php artisan module:filament:install Blog # interactive: cluster? default cluster?
php artisan module:filament:resource # interactive: pick Blog, pick model
Refresh the admin panel. A "Blog" nav group with your resource inside it, with zero edits to the host app's panel provider. That's the value proposition, and it holds up.
filament-modules.mode accepts plugins / panels / both (default both). Pick early, because it changes your file layout and navigation model:
| Mode | What you get | Good for |
|---|---|---|
plugins |
Each module mounts its resources/pages/widgets into the host panel | Monolithic admin, modules are just code organization |
panels |
Each module gets its own panel, linked from the main panel under panels.group |
Hard isolation, per-module auth/theme/branding |
both |
Both of the above | You will get duplicate navigation entries unless you're careful — see Gotchas |
With panels, the config keys panels.group, panels.group-icon, panels.open-in-new-tab, and panels.group-sort do all the navigation work. open-in-new-tab is worth enabling for module panels — it makes the "these are separate apps" mental model obvious to users.
The cluster is what makes a module feel like a feature rather than a folder. Set clusters.enabled: true and clusters.use-top-navigation: true, then let module:filament:install generate Clusters/<Module>/ with Pages, Resources, Widgets beneath it. Every subsequent module:filament:resource prompt offers to place the file in the cluster — always say yes.
use-top-navigation promotes clusters to the top bar and pushes their pages into a side sub-nav. This is the highest-leverage single flag in the package for UX: it turns a flat 40-item sidebar into a small number of top-level domains.
All generators share three aliases each, so pick one and put it in your shell history:
php artisan module:filament:resource # module:make:filament-resource / module:filament:make-resource
php artisan module:filament:page
php artisan module:filament:widget
php artisan module:filament:cluster
php artisan module:filament:plugin
php artisan module:filament:panel
php artisan module:filament:theme # WIP
The generators know about clusters, panels, and the module namespace. Hand-rolling a resource in a module and getting the namespace, cluster membership, and plugin registration right is three separate chances to be wrong — the generator gets all three for free.
A module's MyModulePlugin is the entry point, but module:filament:plugin lets you create additional plugin classes inside the same module. Use this when a module ships several independently toggleable surfaces (e.g. a BlogPlugin and a BlogCommentsPlugin) and the host app may want only one. Note that auto-registration applies to all discovered plugins when auto-register-plugins: true — if you need selective enabling, flip that to false and register the plugin classes you want by hand in the panel.
module:filament:panel generates a panel and a matching Providers/Filament/<Name>PanelServiceProvider.php. Registration of resources to a panel is then plain Filament — discoverResources() / discoverPages() / discoverWidgets() inside that provider's panel() method. The package wires modules to panels; it does not override Filament's per-panel discovery. So to scope a resource to one panel:
Filament/ReportsPanel/Resources).ReportsPanelServiceProvider discover that path.ModulesPlugin on the main panel so the cross-panel links appear.Swap the imports in your generated classes:
use Coolsam\Modules\Resource; // instead of Filament\Resources\Resource
use Coolsam\Modules\Page; // instead of Filament\Pages\Page
use Coolsam\Modules\TableWidget;
use Coolsam\Modules\ChartWidget;
use Coolsam\Modules\StatsOverviewWidget;
Or, if you only want the behaviour on a couple of classes, use CanAccessTrait; directly. Because this is WIP, adopt it module by module rather than retrofitting the whole codebase — and if you're already using a permissions package (Shield, spatie/permission), verify the two don't fight over canAccess() before committing.
The real selling point is portability. A finished module is a Composer package: Modules/Blog/composer.json with a require block, and consuming projects pull it via a VCS/path repository. Two constraints make or break this:
nwidart/laravel-modules and Filament as require entries with compatible ranges, or the host's merge-plugin autoload will silently produce a module that can't boot.App\ namespace from a module. Contracts, events, and config keys only.Don't let module A use Modules\B\... directly. Laravel events + a config/blog.php published from the module are the cheap path; a small interfaces package is the expensive-but-correct path once you have more than ~4 modules.
mode: both produces duplicate navigationThis is the most common first-day bug. In both mode, a module's resources get registered into the host panel and the module gets its own panel with the same resources. You'll see every resource twice. Either commit to plugins mode, or use panels mode with clusters and skip resources at the host level — but don't leave both on "just in case".
php artisan module:make Foo does not update Composer's autoloader. Run composer dump-autoload after creating a module, or you'll get "class not found" on the generated plugin. The merge-plugin entry in composer.json handles the module's own composer.json requires, but the PSR-4 map still needs refreshing.
A cluster groups navigation within a panel; a panel is a separate Filament app entirely. If you set clusters.enabled: true and generate per-panel folders, you can end up with a cluster whose resources belong to a different panel — they simply won't render. Rule of thumb: pick clusters-per-module for a single shared panel, or panels-per-module for isolation. Doing both is only worth it for large domains.
mode value is not validated loudlyAn invalid or misspelled mode string falls back to a default rather than erroring. If navigation looks wrong and nothing else explains it, php artisan config:show filament-modules and compare against the ConfigMode enum's cases.
php artisan optimize:clear is the standard fix, but for module-related weirdness also clear nwidart's own cache (php artisan module:clear-compiler if present in your install, or delete bootstrap/cache/modules.php / modules_statuses.json staleness). Filament component caching (php artisan filament:clear-cached-components on 4.x/5.x) is the other classic culprit when a newly generated resource 404s after you've already visited the panel.
Every generator (module:filament:resource etc.) prompts. There is no documented --module= / --model= non-interactive surface in the README. Practical consequence: you cannot script scaffolding in CI, and you should not try to. Do scaffolding locally and commit.
The README explicitly marks widgets, themes, and the entire access-control layer as WIP. Widgets generate but expect to inspect the output before trusting placement. Themes inside modules are the least mature area — if per-module branding matters, use Filament's panel-level theme config instead of module:filament:theme for now.
v5 requires PHP 8.3+, Laravel 11/12/13, Filament 4/5, laravel-modules 11+. On Filament 3 you must stay on coolsam/modules 4.x; on Laravel 10, 3.x. The package also has a fast Filament 4→5 cadence, so pin an exact minor version in composer.json and read the changelog before bumping — a Filament minor can move enough panel internals to break navigation wiring.
Resource classes, one namespace collisionCoolsam\Modules\Resource and Filament\Resources\Resource both exist. PHP will not warn you — you'll just get a resource that silently can't be found by discovery, or one that skips access control. Adopt a consistent import convention (e.g. always alias: use Coolsam\Modules\Resource as ModuleResource;) so a code review catches a wrong base class instantly.
module:filament:panel writes the provider and the panel class, but the module's own service provider must be loaded (normally automatic via nwidart) for the panel to appear in Laravel's provider graph. If a module panel 404s, check php artisan about / php artisan module:list — the module's service provider not being booted is the usual reason.
Module tests live in Modules/<Name>/tests and run through the module's own PHPUnit config. Two pragmatic tips: (1) point the module's testsuite at the same phpunit.xml schema as the host so CI just works; (2) for Filament resource tests, use livewire() assertions against the generated pages rather than asserting on navigation — navigation wiring is the package's job and is exercised upstream.
212 stars, single maintainer, fast-moving target surface (Filament majors). Budget for the possibility of maintaining a fork or pinning hard. The good news: the surface area you actually depend on is small — ModulesPlugin, a config file, and a set of generator commands. If you had to replace it, a custom PanelProvider loop over nwidart modules gets you 80% of the way back in an afternoon.
How can I help you explore Laravel packages today?