# TenantPilot Filament Guidelines Status: 2026-05-15 Applies to: Filament v5, Livewire v4.1, Laravel 12. ## Version Contract - Livewire v4.0+ compliance: satisfied by Livewire 4.1.4. - Panel provider location: `apps/platform/bootstrap/providers.php` registers `AdminPanelProvider` and `SystemPanelProvider`. - Admin panel path: `/admin`. - System panel path: `/system`. - Filament asset deployment: any registered Filament assets require `cd apps/platform && php artisan filament:assets` in deployment or release build. ## Global Search Contract - A resource may use global search only when it has a View or Edit page and a `$recordTitleAttribute`. - Relationship-backed global search details must eager-load relationships in `getGlobalSearchEloquentQuery()`. - If a resource is tenant-sensitive or lacks safe View/Edit URLs, set `protected static bool $isGloballySearchable = false`. - Current examples: `PolicyResource`, `ProviderConnectionResource`, and `ManagedEnvironmentResource` disable global search, which is correct for sensitive tenant-scoped surfaces. ## Destructive and High-Impact Actions Every destructive or high-impact action must have: - `->action(...)`, not URL-only execution. - `->requiresConfirmation()`. - Policy or gate authorization inside the action handler. - `UiEnforcement` or `WorkspaceUiEnforcement` on the visible/disabled UI state. - Audit log entry. - Success/error notification. - Pest test for visible/disabled/denied/executed behavior. Destructive examples: delete, force delete, restore, archive, retry restore, run restore, disable provider connection, purge, revoke, credential rotation, backup/restore mutations. ## Filament Do's - Use native Filament resources, pages, tables, forms, schemas, actions, relation managers, widgets, clusters, and notifications before custom Blade/JS. - Use render hooks and CSS hook classes instead of publishing internal Filament views. - Keep tables scan-first: default sort, explicit empty state, sensible pagination profile, hidden technical detail columns. - Use `ActionSurfaceDeclaration` when the resource participates in the project action-surface contract. - Keep RelationManagers lazy-loaded unless an operator workflow requires eager loading. - Use policies for model authorization and `UiEnforcement` for UI affordance consistency. - Use `rateLimit()` or Laravel rate limiting for actions that can trigger expensive remote or queued work repeatedly. ## Filament Don'ts - Do not put business workflows directly in long action closures when they mutate data or dispatch remote work. - Do not assume confirmation modals on `->url(...)` actions. - Do not expose user-controlled URLs to `url()` without scheme validation. - Do not use `preserveFilenames()` for uploads on local/public disks. - Do not enable global search on resources that cannot safely link to View/Edit pages. - Do not hide unauthorized UI as the only security control. - Do not add custom pages when a Resource, RelationManager, or action modal covers the workflow. ## Project-Specific Patterns ### Safe Table Action ```php use App\Actions\BackupSchedules\StartBackupScheduleRun; use App\Models\BackupSchedule; use App\Support\Auth\Capabilities; use App\Support\Rbac\UiEnforcement; use Filament\Actions\Action; use Filament\Notifications\Notification; UiEnforcement::forTableAction( Action::make('runNow') ->label('Run now') ->icon('heroicon-o-play') ->requiresConfirmation() ->modalHeading('Run backup schedule now?') ->action(function (BackupSchedule $record, StartBackupScheduleRun $starter): void { $runId = $starter->handle(auth()->user(), $record); Notification::make() ->title('Backup run queued') ->body("Operation run #{$runId} was created.") ->success() ->send(); }), fn (BackupSchedule $record): mixed => $record->managedEnvironment, ) ->requireCapability(Capabilities::TENANT_BACKUP_SCHEDULES_RUN) ->apply(); ``` ### Extracted Schema ```php namespace App\Filament\Resources\BackupScheduleResource\Schemas; use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Toggle; use Filament\Schemas\Schema; final class BackupScheduleForm { public static function configure(Schema $schema): Schema { return $schema->schema([ TextInput::make('name')->required()->maxLength(255), Select::make('frequency')->required()->options([ 'daily' => 'Daily', 'weekly' => 'Weekly', ]), Toggle::make('is_enabled')->label('Enabled'), ]); } } ``` ### Extracted Table ```php namespace App\Filament\Resources\BackupScheduleResource\Tables; use App\Support\Filament\TablePaginationProfiles; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; final class BackupScheduleTable { public static function configure(Table $table): Table { return $table ->defaultSort('next_run_at') ->paginationPageOptions(TablePaginationProfiles::resource()) ->columns([ TextColumn::make('name')->searchable()->sortable(), TextColumn::make('status')->badge(), TextColumn::make('next_run_at')->since()->sortable(), ]) ->emptyStateHeading('No backup schedules') ->emptyStateDescription('Create a schedule after selecting a managed environment.'); } } ``` ## Migration Plan for Bad Patterns 1. Identify resource files above 1,000 LOC or actions above 60 LOC. 2. Extract repeated action orchestration into `app/Actions//...`. 3. Extract table columns/filters/actions into resource-local builder classes only when they reduce review risk. 4. Add policy tests before deleting resource-level authorization logic. 5. Keep one feature branch per refactor slice to avoid broad conflicts. ## Testing Plan - Resource pages and relation managers are Livewire components and must be tested through Pest/Livewire. - Mutating actions must use Filament action testing helpers such as `callAction`, `mountAction`, `callTableAction`, `assertActionDisabled`, and `assertTableActionVisible`. - Browser tests are reserved for critical multi-step workflows, JS errors, accessibility regressions, and visual smoke checks.