savannabits/filament-modules
Organize your Filament admin panel into autonomous Laravel modules that work as plugins, complete with their own resources, pages, widgets, and clusters. A wrapper around nwidart/laravel-modules for easy sharing and reuse.
Version-matrix check first. This is the single biggest install-time failure source. v5.x pairs Laravel 11/12/13 with Filament 4.x/5.x, PHP 8.3+, and nwidart/laravel-modules 11/12/13. If you're on Filament 3, you need the 4.x branch of this package, not v5.
composer require coolsam/modules
The package pulls a compatible nwidart/laravel-modules automatically, but it does not configure it for you. Before touching anything Filament-related:
nwidart/laravel-modules per its own docs (module paths, namespace, stub overrides).composer.json — without this, module autoloading silently fails with Class not found errors later:"extra": {
"laravel": { "dont-discover": [] },
"merge-plugin": { "include": ["Modules/*/composer.json"] }
}
php artisan modules:install # interactive; publishes config + sets up
# or: 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());
}
First use case worth building: a small self-contained module (e.g. Blog) with one Resource and one Cluster. That exercises the whole loop — module creation, Filament scaffolding, auto-discovery, cluster navigation — in about ten minutes, and reveals any autoload or config problems before you invest in a real feature module.
php artisan module:make Blog
php artisan module:filament:install Blog # interactive: clusters? default cluster?
php artisan module:filament:resource # prompt for module + panel/cluster target
Where to look first when something is off, in order: config/filament-modules.php → the generated Modules/Blog/app/Providers/Filament/BlogServiceProvider.php → the generated Modules/Blog/app/Filament/BlogPlugin.php → your AdminPanelProvider.
filament-modules.mode (a ConfigMode enum) drives everything:
| Mode | Behaviour | Use when |
|---|---|---|
plugins |
Each module's *Plugin registers into your existing panel(s) |
Most apps. One admin panel, modules contribute resources to it. |
panels |
Each module gets its own Panel, linked from the main panel nav | Modules are large enough to deserve their own nav context, or you're building a modular monolith with per-domain admins. |
both |
Default — plugins and panels | Only if you genuinely want both; it's the most confusing to reason about during debugging. |
For a typical SaaS admin, start on plugins. Switching to panels later means regenerating module panel providers, so decide early.
ModulesPlugin contractWith auto-register-plugins => true, registering ModulesPlugin::make() on the panel is sufficient — every module plugin and (in panel mode) every module panel link is picked up. Set it to false when you want explicit control per panel:
$panel->plugins([
ModulesPlugin::make(),
\Modules\Blog\Filament\BlogPlugin::make(),
\Modules\Billing\Filament\BillingPlugin::make(),
]);
This is the right choice when modules are feature-flagged per tenant or per environment — you can conditionally include them.
clusters.enabled changes where generated code lands (Modules/X/app/Filament/Clusters/X/Resources vs Modules/X/app/Filament/Resources). Because the namespace follows the path, retrofitting clusters onto an existing module means moving files and rewriting namespace declarations in every generated class. Decide at module:filament:install time and stay consistent.
clusters.use-top-navigation => true is worth enabling in cluster mode: cluster links become top-level tabs with the cluster's own resources as side nav. It reads far better than a flat sidebar of forty items.
# Resources
php artisan module:make:filament-resource # aliases: module:filament:resource, module:filament:make-resource
# Pages
php artisan module:make:filament-page
# Widgets
php artisan module:make:filament-widget
# Clusters
php artisan module:make:filament-cluster
# Plugins
php artisan module:make:filament-plugin
# Panels
php artisan module:make:filament-panel
The triple-alias situation is real: muscle memory from Filament's native make:filament-resource will fail here because the module variant prompts for a module first. Standardise on module:make:filament-* in your team docs so nobody half-remembers module:filament:make-resource.
The important structural difference from vanilla Filament: you are not passing a target directory. The command resolves module → panel → cluster from interactive prompts. That means command output is only as reliable as your config, so verify the generated namespace line matches the file path after each generation during early adoption.
If a module hosts multiple panels, each gets its own *PanelServiceProvider with its own discoverResources() / discoverPages() / discoverWidgets() paths. To keep a resource in one panel, generate it into that panel's subdirectory and let only that provider discover the path:
Modules/Blog/app/Filament/AdminPanel/Resources/PostResource.php
Modules/Blog/app/Filament/PublicPanel/Resources/ArticleResource.php
This package wires modules to panels; it deliberately does not override Filament's per-panel discovery model. Don't expect a magic attribute to move a resource between panels.
Instead of Filament\Resources\Resource, extend the module base classes so module resources participate in the package's access checks:
use Coolsam\Modules\Resource; // instead of Filament\Resources\Resource
use Coolsam\Modules\Page; // instead of Filament\Pages\Page
use Coolsam\Modules\TableWidget; // instead of Filament\Widgets\TableWidget
use Coolsam\Modules\ChartWidget;
use Coolsam\Modules\StatsOverviewWidget;
Or apply CanAccessTrait directly if you need to keep the original parent (e.g. a resource that must extend a domain-specific base class). Note the README marks this section WIP — treat it as "read the trait source before relying on it for real authorization" rather than a security boundary.
The real payoff: a module with composer.json, app/Filament/BlogPlugin.php, its own resources and migrations can be composer require'd into another Laravel app and appear in the panel by virtue of ModulesPlugin alone. Keep modules free of host-app assumptions — reference contracts and config keys, never env() or hardcoded panel class names — and this works. Break that discipline and you get a module that only runs in one repo.
Class "Modules\X\..." not found after creating a module. Almost always the merge-plugin block, or a missing composer dump-autoload. Run composer dump-autoload after every module:make. In CI, remember fresh checkouts need it too — it's not in the lockfile behaviour you'd expect.
Config caching hides your config edits. filament-modules.php is read at panel boot. php artisan config:clear after every change, and never assume a mode switch took effect without clearing.
The install command is interactive. modules:install and module:filament:install prompt for cluster choices. Don't put them in deploy scripts or Dockerfiles — run them once locally, commit the generated files and config. Likewise module:make:filament-* will hang in non-TTY contexts.
Register module panel providers in bootstrap/providers.php. In panel mode, generated *PanelServiceProvider classes need to be in the provider list (Laravel 11+ style) or auto-discovered via the module's service provider. If module panel links don't appear in the main navigation, this is the first thing to check — followed by whether mode actually includes panels.
Navigation grouping only applies in panel mode. panels.group, group-icon, open-in-new-tab, and group-sort do nothing under mode => plugins. Setting them and expecting a sidebar group is a common dead end.
Maturity risk is real. 212 stars, near-zero dependents, fast-moving major versions, and several features explicitly marked WIP (widgets, themes, access control). Pin the exact version in composer.json, read the source of anything WIP before depending on it, and budget time for upgrades between minor releases — the changelog shows frequent patch releases, so ^5.3 with a lockfile is safer than ^5.
Debugging discovery, in order: dump config('filament-modules') → check the module's module.json providers list → confirm the generated plugin class exists and its getId() is unique → verify ModulesPlugin is registered on the panel you're actually viewing. Resource-not-appearing bugs are almost always one of these four, not a Filament bug.
Extension points to prefer over forking: each generated plugin and cluster class is plain PHP you own — override register(), boot(), navigation methods there. The panel provider's discover*() calls are your hook for scoping. Forking the package should be a last resort; the base classes (Coolsam\Modules\Resource etc.) are thin wrappers you can replace if needed.
Keep module migrations and seeders inside the module and run them via module:migrate rather than copying into database/migrations. Copying defeats the exportability that makes this package worth using.
Test module panels as real panels. Because panel mode produces standalone Panel objects, Livewire::test() against a module page without the correct panel context gives false passes or confusing 404s. Set the current panel in tests (or hit the panel route) rather than testing components in isolation.
How can I help you explore Laravel packages today?