Filament at Scale: Multi-Panel Auth, Custom Panels, and Table Query Tuning
#filament #laravel #multi-tenant #performance

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

3 min read Mohamed Said Mohamed Said

Why Multi-Panel Filament Gets Messy Fast

Filament's panel system is powerful, but teams often bolt on a second panel without thinking through auth isolation, query scope, or table performance. The result is shared session state, leaking queries, and N+1 problems hiding behind pretty UI. This article covers the three areas that matter most at scale.


1. Separate Auth Guards Per Panel

Each panel should own its guard. Define them in config/auth.php:

'guards' => [
    'admin' => [
        'driver'   => 'session',
        'provider' => 'admins',
    ],
    'tenant' => [
        'driver'   => 'session',
        'provider' => 'users',
    ],
],
'providers' => [
    'admins' => ['driver' => 'eloquent', 'model' => App\Models\Admin::class],
    'users'  => ['driver' => 'eloquent', 'model' => App\Models\User::class],
],

Then wire each panel to its guard inside the PanelProvider:

// app/Providers/Filament/AdminPanelProvider.php
public function panel(Panel $panel): Panel
{
    return $panel
        ->id('admin')
        ->path('admin')
        ->authGuard('admin')
        ->login()
        ->middleware([
            EncryptCookies::class,
            StartSession::class,
            AuthenticateSession::class,
        ])
        ->authMiddleware([Authenticate::class]);
}

Critical: do not share the default web middleware group between panels unless you want session collisions. Each panel should declare its own middleware stack explicitly.


2. Scoping Resources to the Authenticated Tenant

Global scopes are the cleanest way to ensure every Eloquent query inside a panel is automatically filtered. Register a scope in the panel's boot phase:

// Inside TenantPanelProvider::panel()
->tenant(Team::class, ownershipRelationship: 'team')

For custom scoping beyond Filament's built-in tenancy, override getEloquentQuery() on the resource:

public static function getEloquentQuery(): Builder
{
    return parent::getEloquentQuery()
        ->whereBelongsTo(filament()->getTenant());
}

Avoid putting this logic in a global Eloquent scope unless you also need it outside Filament — mixing panel concerns into your domain models is a maintenance trap.


3. Table Query Tuning: The Three Common Killers

3a. Eager-load relationships used in columns

public static function table(Table $table): Table
{
    return $table
        ->query(
            Order::query()->with(['customer', 'items.product'])
        )
        ->columns([
            TextColumn::make('customer.name'),
            TextColumn::make('items_count')
                ->counts('items'),
        ]);
}

Filament's ->counts() and ->exists() column modifiers push aggregates into the base query rather than triggering per-row subqueries — use them instead of accessor methods.

3b. Defer expensive columns

For columns that require heavy joins or subqueries, mark them as toggleable and hidden by default:

TextColumn::make('lifetime_value')
    ->toggleable(isToggledHiddenByDefault: true)
    ->getStateUsing(fn (Customer $r) => $r->orders()->sum('total')),

This keeps the default page load fast; power users can opt in.

3c. Paginate aggressively and add database indexes

Filament's default page size is 10, but teams often bump it to 50 or 100 without adding indexes on the sort column. Every TextColumn::make('created_at')->sortable() call becomes an ORDER BY created_at — ensure that column is indexed:

$table->index(['team_id', 'created_at']);

A composite index on (team_id, created_at) satisfies both the tenant scope WHERE and the ORDER BY in a single index scan.


Takeaways

  • Assign a dedicated auth guard to every panel and declare middleware stacks explicitly to prevent session bleed.
  • Scope resource queries at the resource level, not in global Eloquent scopes, to keep domain models clean.
  • Use Filament's built-in ->counts() and ->exists() modifiers to push aggregates into SQL rather than PHP.
  • Hide expensive columns behind toggleable(isToggledHiddenByDefault: true) to protect default page load times.
  • Add composite indexes that match your tenant scope column plus the default sort column.

Found this useful?

Frequently Asked Questions

3 questions
Q01 Can two Filament panels share the same Eloquent model for authentication?
Yes, but they should still use separate guards pointing to the same provider. This keeps session cookies and remember tokens isolated between panels, preventing one panel's logout from affecting the other.
Q02 Does Filament's built-in tenancy handle multi-panel setups automatically?
Filament's tenant() helper scopes one panel to a tenant model, but it does not coordinate across multiple panels. You need to configure tenancy independently on each PanelProvider and ensure middleware stacks do not overlap.
Q03 When should I override getEloquentQuery() versus using a global Eloquent scope?
Override getEloquentQuery() when the scope is specific to the Filament panel context. Reserve global Eloquent scopes for rules that must apply everywhere — API, CLI, and UI alike — to avoid unintended filtering outside the panel.

Continue reading

More Articles

View all