-
Organize every form schema with Section and Fieldset first. Start from grouped
layout, then drop fields in — never a flat bag of inputs.
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Fieldset;
use Filament\Forms\Components\TextInput;
$schema->components([
Section::make('Identity')
->description('Who this contact is')
->columns(2)
->schema([
TextInput::make('first_name')->required(),
TextInput::make('last_name'),
]),
Fieldset::make('Contact') // two-column grid by default
->schema([
TextInput::make('email')->email()->required(),
TextInput::make('phone')->tel(),
]),
]);
-
Group pages/resources into navigation menu groups. Set the static property; its type is
string | UnitEnum | null, so prefer a backed enum for type-safe, reusable group names.
use UnitEnum;
protected static string | UnitEnum | null $navigationGroup = 'CRM';
-
Provide a RelationManager for every real relation. Register them in the resource's
getRelations(); each manager declares protected static string $relationship = '...' and
its own form(Schema $schema) + table(Table $table).
public static function getRelations(): array
{
return [TagsRelationManager::class, ActivitiesRelationManager::class];
}
-
Delete belongs on the LIST table, not the Edit page. make:filament-resource puts
DeleteAction::make() in the Edit page's getHeaderActions() by default — remove it.
Put deletion in the table: DeleteAction::make() in ->recordActions([...]) (per-row) and
DeleteBulkAction::make() inside a BulkActionGroup in ->toolbarActions([...]).
v5 table methods are ->recordActions() / ->toolbarActions(); v3's ->actions() /
->bulkActions() still exist as @deprecated aliases that forward to them — prefer the new names.
-
Create and Edit both redirect to the LIST page on save. Two ways:
- Panel-wide (preferred):
->resourceCreatePageRedirect('index')->resourceEditPageRedirect('index').
- Per page: override
getRedirectUrl() to return $this->getResource()::getUrl('index').
Default Filament redirects to view/edit, so this override is required.
-
The top-bar user menu must have a Settings/profile page. Register a profile page on the
panel — ->profile() — and Filament auto-injects the profile item into the user menu. Use a
custom page (->profile(Settings::class)) when you need more than the default account form.
-
Native Filament auth + OPTIONAL authenticator-app (TOTP) 2FA. Enable login with
->login(). For app-based 2FA the user can turn on themselves, register the
AppAuthentication provider with isRequired: false:
use Filament\Auth\MultiFactor\App\AppAuthentication;
$panel->multiFactorAuthentication([
AppAuthentication::make()->recoverable(),
]);
The User model must implement Filament\Auth\MultiFactor\App\Contracts\HasAppAuthentication
(and HasAppAuthenticationRecovery for recovery codes). The enable/disable + QR-code UI is
served by the profile page from rule 6 — no custom security page to build.
-
No public registration. Simply do not call ->registration() on the panel. Seed users
with php artisan make:filament-user or your own invite flow.
-
The dashboard is a custom page — never the stock one. Filament's default
Filament\Pages\Dashboard auto-discovers and arranges widgets for you, which costs you control.
Generate your own page extending the base Dashboard so you decide exactly which widgets show,
in what order, and the column layout. Override getWidgets() and getColumns() (it accepts a
responsive array); getHeaderWidgets()/getFooterWidgets() for the top/bottom slots.
namespace App\Filament\Pages;
use Filament\Pages\Dashboard as BaseDashboard;
class Dashboard extends BaseDashboard
{
public function getWidgets(): array
{
return [StatsOverview::class, RevenueChart::class, LatestContacts::class];
}
public function getColumns(): int | array
{
return ['md' => 2, 'xl' => 3];
}
}
Register the custom page on the panel — ->pages([\App\Filament\Pages\Dashboard::class]) — and
make sure the stock Filament\Pages\Dashboard::class is not also registered (page
discovery + an explicit entry can yield two dashboards). Grounded in Filament\Pages\Dashboard
(vendor/filament/filament/src/Pages/Dashboard.php: class Dashboard extends Page; getWidgets(),
getColumns(): int | array).
If the Resource/panel you're editing lives in a symlinked composer package (per the repo's
package rules), make the change in that package's own folder and commit/push there — never
patch the consumer's vendor/ copy.