The Problem With One Panel for Everything
Most Filament tutorials show a single AdminPanelProvider. That works until you need a customer-facing portal sitting beside your internal admin, each with its own user model, guard, and middleware stack. Bolting both concerns onto one panel produces a tangled mess of policy checks and route conflicts.
The cleaner path: register two discrete panels, each owning its auth contract.
Registering a Second Panel
Filament resolves panels through service providers. Create a dedicated provider for each panel.
php artisan make:filament-panel customer
This scaffolds app/Providers/Filament/CustomerPanelProvider.php. Configure it independently:
public function panel(Panel $panel): Panel
{
return $panel
->id('customer')
->path('portal')
->authGuard('customer') // dedicated guard
->login(CustomerLogin::class) // custom login page
->colors(['primary' => Color::Teal])
->discoverResources(
in: app_path('Filament/Customer/Resources'),
for: 'App\\Filament\\Customer\\Resources'
)
->middleware([
EncryptCookies::class,
VerifyCsrfToken::class,
SubstituteBindings::class,
])
->authMiddleware([Authenticate::class]);
}
Register both providers in bootstrap/providers.php (Laravel 11+) or config/app.php.
Separate Guards and User Models
// config/auth.php
'guards' => [
'web' => ['driver' => 'session', 'provider' => 'users'],
'customer' => ['driver' => 'session', 'provider' => 'customers'],
],
'providers' => [
'users' => ['driver' => 'eloquent', 'model' => App\Models\User::class],
'customers' => ['driver' => 'eloquent', 'model' => App\Models\Customer::class],
],
Filament calls auth()->guard($panel->getAuthGuard()) internally, so the panel's guard name is the only coupling point.
Sharing Resources Across Panels
Occasionally an OrderResource belongs in both panels but with different column sets. Rather than duplicating the class, use a base resource and extend it:
// App\Filament\Base\BaseOrderResource.php
abstract class BaseOrderResource extends Resource
{
protected static string $model = Order::class;
public static function baseColumns(): array
{
return [
TextColumn::make('id')->sortable(),
TextColumn::make('total')->money('usd'),
];
}
}
// App\Filament\Admin\Resources\OrderResource.php
class OrderResource extends BaseOrderResource
{
public static function table(Table $table): Table
{
return $table->columns([
...static::baseColumns(),
TextColumn::make('customer.email'),
]);
}
}
Table Query Tuning at Scale
Filament tables call paginate() on the Eloquent builder. On a table with 500k rows, the default COUNT(*) for pagination becomes expensive fast.
Override the Table Query
Scope the query at the resource level to avoid full-table scans:
public static function getEloquentQuery(): Builder
{
return parent::getEloquentQuery()
->select(['id', 'status', 'total', 'created_at', 'customer_id'])
->with('customer:id,email') // eager-load only needed columns
->where('created_at', '>=', now()->subYear());
}
Disable Count-Based Pagination
Filament v3 supports ->paginationPageOptions([25, 50]) but still fires a count query. For very large tables, switch to simple pagination:
public static function table(Table $table): Table
{
return $table
->paginated([25, 50])
->defaultPaginationPageOption(25)
->query(fn () => static::getEloquentQuery())
// Filament respects simplePaginate when you override the paginator:
->paginateUsing(fn (Builder $query, int $page, int $perPage) =>
$query->simplePaginate($perPage, ['*'], 'page', $page)
);
}
Index Your Sort Columns
Every sortable column fires an ORDER BY. Ensure composite indexes cover the sort + filter combination:
CREATE INDEX orders_status_created_at_idx ON orders (status, created_at DESC);
Run EXPLAIN ANALYZE in PostgreSQL or EXPLAIN FORMAT=JSON in MySQL to confirm the index is used.
Takeaways
- Register each panel in its own provider with a dedicated auth guard and user model — never share guards between panels.
- Use abstract base resources to share schema logic without duplicating Eloquent models.
- Override
getEloquentQuery()to select only required columns and constrain result sets before Filament paginates. - Replace
paginate()withsimplePaginate()viapaginateUsing()on high-volume tables to eliminate the expensiveCOUNT(*)query. - Add composite indexes on every column combination used for filtering and sorting in your tables.