Per-Tenant Filament Panel Themes and Runtime Config | Mohamed Said        [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com) [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles  ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

       [  ](https://github.com/EG-Mohamed)       

 [ Home ](https://msaied.com) [ Projects ](https://msaied.com/projects) [ Articles ](https://msaied.com/articles) [ Certificates ](https://msaied.com/certificates) [ Contact ](https://msaied.com#contact-section) 

  [ home ](https://msaied.com)    [ articles ](https://msaied.com/articles)    Multi-Tenant SaaS with Filament: Per-Tenant Panel Themes and Dynamic Config at Runtime        On this page       1. [  The Problem With Static Panel Configuration ](#the-problem-with-static-panel-configuration)
2. [  Resolving the Tenant Early ](#resolving-the-tenant-early)
3. [  Dynamically Configuring the Panel ](#dynamically-configuring-the-panel)
4. [  Preventing State Leakage on Octane ](#preventing-state-leakage-on-octane)
5. [  Caching Tenant Settings Without Cross-Tenant Pollution ](#caching-tenant-settings-without-cross-tenant-pollution)
6. [  Navigation Customisation Per Tenant ](#navigation-customisation-per-tenant)
7. [  Key Takeaways ](#key-takeaways)

  ![Multi-Tenant SaaS with Filament: Per-Tenant Panel Themes and Dynamic Config at Runtime](https://cdn.msaied.com/301/88c73195f8f12fd047dc2683b2105ead.png)

  #laravel   #filament   #multi-tenant   #saas   #octane  

 Multi-Tenant SaaS with Filament: Per-Tenant Panel Themes and Dynamic Config at Runtime 
========================================================================================

     27 Jun 2026      3 min read    ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said  

       Table of contents

1. [  01   The Problem With Static Panel Configuration  ](#the-problem-with-static-panel-configuration)
2. [  02   Resolving the Tenant Early  ](#resolving-the-tenant-early)
3. [  03   Dynamically Configuring the Panel  ](#dynamically-configuring-the-panel)
4. [  04   Preventing State Leakage on Octane  ](#preventing-state-leakage-on-octane)
5. [  05   Caching Tenant Settings Without Cross-Tenant Pollution  ](#caching-tenant-settings-without-cross-tenant-pollution)
6. [  06   Navigation Customisation Per Tenant  ](#navigation-customisation-per-tenant)
7. [  07   Key Takeaways  ](#key-takeaways)

 The Problem With Static Panel Configuration
-------------------------------------------

Most Filament multi-tenant guides stop at scoping Eloquent queries. That solves data isolation, but a real SaaS product often needs per-tenant branding: different primary colours, logos, navigation groups, and even feature flags that hide entire resources. Doing this safely — without state leaking between requests on Octane or FPM — requires a deliberate approach.

Resolving the Tenant Early
--------------------------

Before Filament boots its panel, you need to know which tenant owns the request. A dedicated middleware registered in `bootstrap/app.php` is the right place:

```php
// app/Http/Middleware/IdentifyTenant.php
public function handle(Request $request, Closure $next): Response
{
    $subdomain = explode('.', $request->getHost())[0];

    $tenant = Tenant::where('subdomain', $subdomain)
        ->with('settings')
        ->firstOrFail();

    // Bind to the container for this request lifecycle only
    app()->instance(Tenant::class, $tenant);

    return $next($request);
}

```

Using `instance()` here is intentional. On FPM every request gets a fresh container. On Octane you must reset this binding — covered below.

Dynamically Configuring the Panel
---------------------------------

Filament panels are configured inside a `PanelProvider`. The trick is to defer any tenant-specific calls until after the request is resolved, using a `booted` callback:

```php
// app/Providers/Filament/AppPanelProvider.php
public function panel(Panel $panel): Panel
{
    return $panel
        ->id('app')
        ->path('app')
        ->bootUsing(function (Panel $panel) {
            $tenant = app(Tenant::class);

            $panel
                ->colors([
                    'primary' => $tenant->settings->primary_color ?? '#6366f1',
                ])
                ->brandName($tenant->name)
                ->brandLogo($tenant->settings->logo_url);

            if ($tenant->settings->feature_reports) {
                $panel->resources([
                    ...config('filament.default_resources'),
                    ReportResource::class,
                ]);
            }
        });
}

```

`bootUsing` runs once per request after the container is warm, so `app(Tenant::class)` resolves the bound instance correctly.

Preventing State Leakage on Octane
----------------------------------

Octane workers are long-lived. An `instance()` binding from request A will still be present for request B unless you flush it. Register a terminating callback in your `AppServiceProvider`:

```php
// app/Providers/AppServiceProvider.php
public function boot(): void
{
    if (app()->bound(\Laravel\Octane\Octane::class)) {
        app(\Laravel\Octane\Contracts\OperationTerminated::class, function () {
            app()->forgetInstance(Tenant::class);
        });
    }
}

```

Alternatively, use Octane's `RequestHandled` event listener to flush the binding after every response.

Caching Tenant Settings Without Cross-Tenant Pollution
------------------------------------------------------

Hitting the database on every request for tenant settings is wasteful. Use a tagged cache key scoped to the tenant:

```php
$settings = Cache::remember(
    "tenant:{$tenant->id}:settings",
    now()->addMinutes(15),
    fn () => $tenant->settings
);

```

When a tenant updates their branding, fire a `TenantSettingsUpdated` event and forget the key:

```php
Cache::forget("tenant:{$tenant->id}:settings");

```

This keeps the cache flat — no tags required — and avoids the tag-flush overhead on Redis Cluster.

Navigation Customisation Per Tenant
-----------------------------------

Filament's `navigationGroups` and `navigationItems` can also be driven by tenant config:

```php
->bootUsing(function (Panel $panel) {
    $tenant = app(Tenant::class);

    $groups = collect($tenant->settings->nav_groups ?? [])
        ->map(fn ($label) => NavigationGroup::make($label)->collapsible())
        ->all();

    $panel->navigationGroups($groups);
})

```

Store nav group order as a JSON column on the settings model and cast it to an array. Tenants can reorder groups through their own settings resource without a deploy.

Key Takeaways
-------------

- Bind the resolved `Tenant` model via `app()->instance()` in middleware, not in a singleton.
- Use `bootUsing()` on the Filament panel to apply tenant config after the container is warm.
- On Octane, explicitly forget the `Tenant` binding after each request to prevent state leakage.
- Cache tenant settings with a per-tenant key and invalidate on update — avoid global cache flushes.
- Store feature flags and nav config as JSON on the tenant settings model for zero-deploy customisation.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fmulti-tenant-saas-with-filament-per-tenant-panel-themes-and-dynamic-config-at-runtime&text=Multi-Tenant+SaaS+with+Filament%3A+Per-Tenant+Panel+Themes+and+Dynamic+Config+at+Runtime) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fmsaied.com%2Farticles%2Fmulti-tenant-saas-with-filament-per-tenant-panel-themes-and-dynamic-config-at-runtime) 

 Frequently Asked Questions 
----------------------------

  3 questions  

     Q01  Why use bootUsing instead of configuring the panel directly in the panel() method?        The panel() method runs during service provider registration, before the request is fully resolved. bootUsing defers execution until after the container has the tenant binding, so app(Tenant::class) returns the correct instance. 

      Q02  Is it safe to call app()-&gt;instance(Tenant::class, $tenant) inside middleware on Octane?        Only if you flush the binding after the response. Use Octane's RequestHandled event or a terminating callback to call app()-&gt;forgetInstance(Tenant::class), otherwise the binding persists for the next request on the same worker. 

      Q03  Can I conditionally register Filament resources per tenant without affecting other tenants?        Yes. Inside bootUsing you have full access to the Panel instance. Call $panel-&gt;resources([...]) with a tenant-specific list. Because bootUsing runs per request, each tenant gets its own resource set without polluting a shared singleton. 

  Continue reading

 More Articles 
---------------

 [ View all    ](https://msaied.com/articles) 

 [ ![Eloquent N+1 at Scale: Eager Loading Strategies, Subquery Selects, and Lazy Eager Loading](https://cdn.msaied.com/302/40500f25f8a29e6cd6eac4938f7211d0.png) laravel eloquent performance 

### Eloquent N+1 at Scale: Eager Loading Strategies, Subquery Selects, and Lazy Eager Loading

N+1 queries silently kill throughput in production. This guide goes beyond basic with() calls to cover subquer...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 27 Jun 2026     4 min read  

  Read    

 ](https://msaied.com/articles/eloquent-n1-at-scale-eager-loading-strategies-subquery-selects-and-lazy-eager-loading) [ ![Help Make Filament Faster: Beta Versions of v4 and v5 Now Available for Testing](https://cdn.msaied.com/299/b7163ad1d319ebdd0e7128cc976053bf.png) Filament Laravel Performance 

### Help Make Filament Faster: Beta Versions of v4 and v5 Now Available for Testing

The Filament team has released performance-focused beta versions of both v4 and v5. Install them today, test i...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 26 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/help-make-filament-faster-beta-versions-of-v4-and-v5-now-available-for-testing) [ ![Filament v4 Schema-Based Forms, Infolists, and the Unified Schema API](https://cdn.msaied.com/297/6eb3a7aaf7148fd21116eea870bd004e.png) filament laravel filament-v4 

### Filament v4 Schema-Based Forms, Infolists, and the Unified Schema API

Filament v4 replaces scattered form and infolist definitions with a single Schema API. Learn how unified schem...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 26 Jun 2026     3 min read  

  Read    

 ](https://msaied.com/articles/filament-v4-schema-based-forms-infolists-and-the-unified-schema-api-1) 

   [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://msaied.com)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

Explore

- [Home](https://msaied.com)
- [Projects](https://msaied.com/projects)
- [Articles](https://msaied.com/articles)
- [Certificates](https://msaied.com/certificates)
- [Contact](https://msaied.com#contact-section)

Connect

- [   hello@msaied.com ](mailto:hello@msaied.com)
- [   +20 109 461 9204 ](tel:+201094619204)

© 2026 Mohamed Said. All rights reserved.

 [  ](https://github.com/EG-Mohamed) [  ](https://www.linkedin.com/in/msaiedm/) [  ](https://wa.me/201094619204) [  ](mailto:hello@msaied.com) [  ](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
