Skip to content

Filament v4 at Scale: Multi-Panel Auth, Custom Panels, and Table Query Tuning

Running Filament v4 across multiple panels with distinct auth guards and thousands of rows? This guide covers custom panel registration, per-panel authentication, and practical table query tuning to keep response times tight.

Published · Updated · 3 min read

Filament v4 at Scale: Multi-Panel Auth, Custom Panels, and Table Query Tuning

The Problem With One Panel Fits All

Most Filament tutorials assume a single /admin panel. Real SaaS products need at least two: one for customers, one for internal staff — each with its own auth guard, middleware stack, and resource set. Filament v4's PanelProvider architecture makes this clean, but there are sharp edges around query performance once you push table row counts past a few thousand.

This article covers three concrete areas:

  1. Registering and isolating multiple panels
  2. Wiring per-panel authentication guards
  3. Tuning table queries so Filament doesn't become your slowest endpoint

Registering Multiple Panels

Each panel gets its own PanelProvider. Create two:

php artisan make:filament-panel customer
php artisan make:filament-panel staff

This generates app/Providers/Filament/CustomerPanelProvider.php and StaffPanelProvider.php. Register both in bootstrap/providers.php:

return [
    App\Providers\Filament\CustomerPanelProvider::class,
    App\Providers\Filament\StaffPanelProvider::class,
];

Each provider calls Panel::make() with a unique ID and path:

public function panel(Panel $panel): Panel
{
    return $panel
        ->id('customer')
        ->path('portal')
        ->login()
        ->resources([
            App\Filament\Customer\Resources\OrderResource::class,
        ]);
}

Per-Panel Authentication Guards

Filament v4 exposes ->authGuard() on the panel builder. Pair it with a dedicated guard in config/auth.php:

// config/auth.php
'guards' => [
    'customer' => [
        'driver'   => 'session',
        'provider' => 'customers',
    ],
    'staff' => [
        'driver'   => 'session',
        'provider' => 'staff',
    ],
],
'providers' => [
    'customers' => ['driver' => 'eloquent', 'model' => App\Models\Customer::class],
    'staff'     => ['driver' => 'eloquent', 'model' => App\Models\StaffUser::class],
],

Then in each panel provider:

return $panel
    ->id('customer')
    ->path('portal')
    ->authGuard('customer')
    ->login(CustomerLoginPage::class);

Filament will resolve Auth::guard('customer') for all authorization checks inside that panel. Sessions are fully isolated — a customer cookie never authenticates a staff request.

Middleware Isolation

Add panel-specific middleware to avoid leaking staff-only rate limits or audit logging into the customer panel:

->middleware([
    EncryptCookies::class,
    StartSession::class,
    App\Http\Middleware\LogCustomerActivity::class,
])
->authMiddleware([
    Authenticate::class,
])

Table Query Tuning

Filament's Table component calls ->query() on every page load. The default is Model::query() — no eager loads, no scopes. At scale that's a disaster.

Scoped Default Query

Always scope to the authenticated tenant and eager-load relations the table columns touch:

public static function getEloquentQuery(): Builder
{
    return parent::getEloquentQuery()
        ->with(['customer', 'items.product'])
        ->where('tenant_id', Filament::getTenant()?->id)
        ->withoutGlobalScope(SoftDeletingScope::class);
}

Deferring Expensive Counts

Filament renders a row count badge by default. On large tables this fires a COUNT(*) on every render. Disable it when the table is filtered by default and the count is meaningless:

Table::make()
    ->paginated([25, 50, 100])
    ->paginationPageOptions([25, 50])
    ->defaultPaginationPageOption(25)
    ->extremePaginationLinks(false)
    ->queryStringIdentifier('orders')

For the count itself, override getTableRecordsPerPageSelectOptions or use a database view with a materialized count column rather than a live aggregate.

Column-Level Eager Loading

Filament v4 columns support ->relationship() declarations. Use them so the table builder can batch-load only what's rendered:

TextColumn::make('customer.name')
    ->label('Customer')
    ->searchable(query: fn (Builder $q, string $s) =>
        $q->whereHas('customer', fn ($q) => $q->where('name', 'like', "%{$s}%"))
    ),

Avoid ->getStateUsing() callbacks that hit the database per row — they bypass Eloquent's eager loading entirely.


Takeaways

  • Register each panel in its own PanelProvider; use ->authGuard() to fully isolate sessions.
  • Scope getEloquentQuery() to the tenant and eager-load every relation the table touches.
  • Disable or defer row counts on large tables; a live COUNT(*) on 500k rows is expensive.
  • Keep panel-specific middleware in the panel provider, not in global app/Http/Kernel.php.
  • Use ->relationship() on columns so Filament can batch-load relations rather than resolving them per row.

Frequently asked questions

Can two Filament v4 panels share the same Eloquent model but use different guards?

Yes. Guards are independent of models. You can point two guards at the same `User` model with different session drivers or providers, then assign each guard to a separate panel via `->authGuard()`. Sessions remain isolated because Filament resolves auth through the panel's configured guard, not the default `web` guard.

How do I prevent N+1 queries in Filament table columns that use ->relationship()?

Override `getEloquentQuery()` in your resource and call `->with([...])` for every relation your columns reference. Filament does not auto-detect required eager loads from column definitions, so explicit `with()` calls in the base query are the most reliable approach.

Mohamed Said

About the author

Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

About GitHub LinkedIn WhatsApp Email Address My CV

Have a technical challenge?

Tell me what you’re building. I reply within two working days.

Discuss your project