Host wiring
The installer plugin ships the /install flow, the installer_locks row, and the InstallerUserCreated event. It does not know which seeders your app needs, or that your first user should be super_admin. That is host wiring — set in the app's AppServiceProvider.
Why ordering matters
InstallerState::createUser() dispatches InstallerUserCreated, which calls $event->user->assignRole('super_admin'). Spatie permission throws if the role does not exist yet:
There is no role named super_admin for guard web
The role must exist before the user is created. The installer runs migrate() → seed() → createUser() → lock(), in that order (see InstallController::run). So installer.seeders must include a RoleSeeder that creates super_admin before createUser fires.
The wiring
From the template's app/Providers/AppServiceProvider.php:
use Illuminate\Support\Facades\Event;
use Mamenein\FilamentInstaller\Events\InstallerUserCreated;
public function register(): void
{
// Roles + Passport personal-access client before installer creates the first user.
$this->app->booting(function (): void {
config([
'installer.seeders' => [
\Database\Seeders\RoleSeeder::class,
\Database\Seeders\PassportClientSeeder::class,
],
]);
});
}
public function boot(): void
{
Event::listen(InstallerUserCreated::class, static function (InstallerUserCreated $event): void {
if (! method_exists($event->user, 'assignRole')) {
return;
}
if (class_exists(\Spatie\Permission\Models\Role::class)) {
\Spatie\Permission\Models\Role::findOrCreate('super_admin');
}
$event->user->assignRole('super_admin');
});
}
The config is written in a booting callback so it lands before the installer's own boot logic reads installer.seeders. The findOrCreate inside the listener is a safety net only — the seeder is the real source of the role.
Shield: roles are not enough
RoleSeeder must generate Shield permission rows, not only role names. A role with empty permissions makes the Roles UI count 0 and RolePolicy 403 on every action. The template's current RoleSeeder is still names-only (Role::findOrCreate('super_admin'), Role::findOrCreate('user')); closing that gap is tracked against the template. See Shield and RBAC for the matrix RoleSeeder must produce.
Flow
sequenceDiagram
participant App as AppServiceProvider
participant Inst as /install
participant State as InstallerState
participant DB
App->>App: booting → config installer.seeders
Inst->>State: run()
State->>DB: migrate --force
State->>DB: seed RoleSeeder (super_admin)
State->>DB: seed PassportClientSeeder
State->>State: createUser → dispatch InstallerUserCreated
State->>DB: assignRole(super_admin)
State->>DB: lock() → insert installer_locks
Inst-->>Inst: redirect to complete
See also: