Per-Tenant Filament Panel Themes and Runtime Config | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://www.msaied.com/public) - [Home](https://www.msaied.com/public)
- [Projects](https://www.msaied.com/public/projects)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [About](https://www.msaied.com/public#about)

           [  Contact](https://www.msaied.com/public#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://www.msaied.com/public)
- [ProjectsCase studies](https://www.msaied.com/public/projects)
- [ArticlesEngineering notes](https://www.msaied.com/public/articles)
- [CertificatesCredentials](https://www.msaied.com/public/certificates)
- [AboutHow I work](https://www.msaied.com/public#about)
- [ContactGet in touch](https://www.msaied.com/public#contact)

  [Start a conversation](https://www.msaied.com/public#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://www.msaied.com/public)
2. /
3. [Articles](https://www.msaied.com/public/articles)
4. /
5. Multi-Tenant SaaS with Filament: Per-Tenant Panel Themes and Dynamic Config at Runtime

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

 Go beyond basic tenant scoping. Learn how to swap Filament panel colours, navigation, and config values per tenant at runtime without polluting the service container or leaking state between requests.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://www.msaied.com/public#person) Published 27 Jun 2026 · Updated 27 Jun 2026 · 3 min read

ShareCopy linkCopied

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

  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)

 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.

- [laravel](https://www.msaied.com/public/articles?search=laravel)
- [filament](https://www.msaied.com/public/articles?search=filament)
- [multi-tenant](https://www.msaied.com/public/articles?search=multi-tenant)
- [saas](https://www.msaied.com/public/articles?search=saas)
- [octane](https://www.msaied.com/public/articles?search=octane)

 Frequently asked questions 
---------------------------

  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.

   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.

   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.

   ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)About the author
----------------

[Mohamed Said](https://www.msaied.com/public#person)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](https://www.msaied.com/public#about) [GitHub ↗](https://github.com/EG-Mohamed) [LinkedIn ↗](https://www.linkedin.com/in/msaiedm/) [WhatsApp ↗](https://wa.me/201094619204) [Email Address ↗](mailto:hello@msaied.com) [My CV ↗](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)  

   [Previous articleHelp Make Filament Faster: Beta Versions of v4 and v5 Now Available for Testing](https://www.msaied.com/public/articles/help-make-filament-faster-beta-versions-of-v4-and-v5-now-available-for-testing) [Next articleEloquent N+1 at Scale: Eager Loading Strategies, Subquery Selects, and Lazy Eager Loading](https://www.msaied.com/public/articles/eloquent-n1-at-scale-eager-loading-strategies-subquery-selects-and-lazy-eager-loading)  

   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)

 ###  Have a technical challenge?

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

[Start a conversation](https://www.msaied.com/public#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/740/cce86edc21eddcbdd2f2454fadaf9c70.png)  · 3 min read### The Pipeline Pattern in Laravel: Custom Pipelines Beyond Middleware

5 Oct 2026 ](https://www.msaied.com/public/articles/the-pipeline-pattern-in-laravel-custom-pipelines-beyond-middleware-1) [ ![](https://cdn.msaied.com/739/2d6897fdcdcf090613f96f72a64b8a78.png)  · 4 min read### MySQL Full-Text Search in Laravel: Indexes, Relevance Scoring, and Boolean Mode

4 Oct 2026 ](https://www.msaied.com/public/articles/mysql-full-text-search-in-laravel-indexes-relevance-scoring-and-boolean-mode) [ ![](https://cdn.msaied.com/738/073696a3fefe18bec825beec5ac658f5.png)  · 4 min read### Laravel Queue Rate-Limited Middleware: Throttling Jobs Without Losing Work

4 Oct 2026 ](https://www.msaied.com/public/articles/laravel-queue-rate-limited-middleware-throttling-jobs-without-losing-work) 

  Have a technical challenge?
----------------------------

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

 [Discuss your project ↗](https://www.msaied.com/public#contact) 

  © 2026 Mohamed Said · Built with Laravel, meant to last.Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

 - [Home](https://www.msaied.com/public)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [GitHub](https://github.com/EG-Mohamed)
- [LinkedIn](https://www.linkedin.com/in/msaiedm/)
- [WhatsApp](https://wa.me/201094619204)
- [Email Address](mailto:hello@msaied.com)
- [My CV](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
- [Sitemap](https://www.msaied.com/public/sitemap.xml)
