PHPackages                             leek/filament-subtenant-scope - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. leek/filament-subtenant-scope

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

leek/filament-subtenant-scope
=============================

Second-level tenancy scope (e.g. service area, region, location) for Filament panels — topnav dropdown filter that scopes Eloquent queries globally.

v1.0.0(1mo ago)24165—5%3MITPHPPHP ^8.2

Since May 2Pushed 1mo agoCompare

[ Source](https://github.com/leek/filament-subtenant-scope)[ Packagist](https://packagist.org/packages/leek/filament-subtenant-scope)[ RSS](/packages/leek-filament-subtenant-scope/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (1)Dependencies (7)Versions (2)Used By (0)

Filament Subtenant Scope
========================

[](#filament-subtenant-scope)

Second-level tenancy for [Filament](https://filamentphp.com/) panels. Adds a topnav dropdown that scopes every Eloquent query in the panel to a sub-tenant — service area, region, location, branch, department — without touching individual resources.

Filament's built-in tenancy gives you one tenant. This plugin adds *another* level on top: pick a sub-scope, and resources, widgets, navigation badges, and global search all auto-filter.

[![screenshot](https://private-user-images.githubusercontent.com/60204/586745060-f38cdd23-2c9d-4749-b4f8-b53ebc01534a.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3ODA4NTE5NjMsIm5iZiI6MTc4MDg1MTY2MywicGF0aCI6Ii82MDIwNC81ODY3NDUwNjAtZjM4Y2RkMjMtMmM5ZC00NzQ5LWI0ZjgtYjUzZWJjMDE1MzRhLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNjA2MDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjYwNjA3VDE3MDEwM1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTNhNjdiMzQ0Y2QwYTYzMzk4ZWFiYzAxY2Q0MjhmYjE4NDMwMmE2Y2MwNGNhZDY3YzVmMWM2OTFlNGU5NGRlOTAmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JnJlc3BvbnNlLWNvbnRlbnQtdHlwZT1pbWFnZSUyRnBuZyJ9.bY9p9iUR_3bPO4xcTVNBz1pg53SGmglalEj4aCGaI3c)](https://private-user-images.githubusercontent.com/60204/586745060-f38cdd23-2c9d-4749-b4f8-b53ebc01534a.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3ODA4NTE5NjMsIm5iZiI6MTc4MDg1MTY2MywicGF0aCI6Ii82MDIwNC81ODY3NDUwNjAtZjM4Y2RkMjMtMmM5ZC00NzQ5LWI0ZjgtYjUzZWJjMDE1MzRhLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNjA2MDclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjYwNjA3VDE3MDEwM1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTNhNjdiMzQ0Y2QwYTYzMzk4ZWFiYzAxY2Q0MjhmYjE4NDMwMmE2Y2MwNGNhZDY3YzVmMWM2OTFlNGU5NGRlOTAmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JnJlc3BvbnNlLWNvbnRlbnQtdHlwZT1pbWFnZSUyRnBuZyJ9.bY9p9iUR_3bPO4xcTVNBz1pg53SGmglalEj4aCGaI3c)Why
---

[](#why)

You already have multi-tenancy (e.g. `Company`). Inside each company you also need a soft filter — "show me only the North service area" — that:

- persists across navigation
- survives logout/login
- is shareable via URL
- applies to every model query without per-resource code

This plugin does that with one trait + one `->scopes([...])` array.

Requirements
------------

[](#requirements)

- PHP 8.2+
- Filament v4.x or v5.x
- Livewire v3 or v4

Installation
------------

[](#installation)

```
composer require leek/filament-subtenant-scope
```

### Styles

[](#styles)

Tell your panel theme to compile the plugin's blade utility classes by adding a `@source` directive to the panel theme configured with `->viteTheme(...)`:

```
@import '../../../../vendor/filament/filament/resources/css/theme.css';

@source '../../../../vendor/leek/filament-subtenant-scope/resources/views/**/*.blade.php';
```

Then rebuild your app assets:

```
npm run build
```

Without this, responsive utilities like `hidden sm:inline` used inside the dropdown won't be compiled into your panel CSS and the dropdown label may collapse on wide screens.

### Register the plugin

[](#register-the-plugin)

Register the plugin on your panel and define one or more scopes:

```
use Filament\Panel;
use Leek\FilamentSubtenantScope\SubtenantScope;
use Leek\FilamentSubtenantScope\SubtenantScopingPlugin;
use App\Models\ServiceArea;

public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->plugin(
            SubtenantScopingPlugin::make()
                ->scopes([
                    SubtenantScope::make('service_area', 'Service Area', ServiceArea::class, 'service_area_id')
                        ->icon('heroicon-o-map-pin')
                        ->labelAttribute('name')
                        ->optionsQuery(fn ($user) => ServiceArea::query()
                            ->where('company_id', $user->company_id)
                            ->where('is_active', true)
                            ->orderBy('name')),
                ]),
        );
}
```

That's the whole topnav setup. The dropdown renders next to the global search.

Opt resources into the scope
----------------------------

[](#opt-resources-into-the-scope)

Add the `HasSubtenantScopes` trait and map each scope key to the FK column on the resource's model:

```
use Filament\Resources\Resource;
use Leek\FilamentSubtenantScope\Concerns\HasSubtenantScopes;

class AppointmentResource extends Resource
{
    use HasSubtenantScopes;

    /** @var array */
    protected static array $subTenantScopes = [
        'service_area' => 'service_area_id',
    ];
}
```

The plugin walks every resource in the panel during `boot()` and registers an Eloquent global scope on the model. **Once any resource opts in, all queries on that model auto-filter** — list pages, relation managers, navigation badges, widgets, global search.

### Custom join logic

[](#custom-join-logic)

If the FK isn't on the model directly, pass `null` and define a static method named `scopeSubTenant{Key}`:

```
class ClientProfileResource extends Resource
{
    use HasSubtenantScopes;

    protected static array $subTenantScopes = ['service_area' => null];

    public static function scopeSubTenantServiceArea(Builder $query, int $id): void
    {
        $query->where(function ($q) use ($id) {
            $q->where('primary_service_area_id', $id)
                ->orWhereHas('serviceAreas', fn ($q) => $q->where('service_areas.id', $id));
        });
    }
}
```

Behavior
--------

[](#behavior)

- **URL bookmarks**: append `?scope_=` to any panel URL — the value is read, persisted, then stripped from the URL on the next render so it's sticky.
- **Single option**: when the user has access to exactly one option, the scope renders as a static label (no dropdown) and applies no filter — there's nothing to filter between.
- **Multiple options**: dropdown with "All …" plus each option.
- **No options**: nothing renders.

Persistence
-----------

[](#persistence)

By default, selections persist for the session. To make them sticky across sessions/devices, register get/set callbacks. The classic pattern is a JSON column on `users`:

```
SubtenantScopingPlugin::make()
    ->scopes([/* ... */])
    ->persistUsing(
        get: fn ($user, string $key) => $user->settings['sub_tenant_scopes'][$key] ?? null,
        set: function ($user, string $key, ?int $id): void {
            $settings = $user->settings ?? [];
            $settings['sub_tenant_scopes'][$key] = $id;
            $user->settings = $settings;
            $user->saveQuietly();
        },
    );
```

Resolution order: URL param → session → user storage. First non-null wins.

Customizing the dropdown
------------------------

[](#customizing-the-dropdown)

### Render hook

[](#render-hook)

Override where the dropdown renders:

```
use Filament\View\PanelsRenderHook;

SubtenantScopingPlugin::make()
    ->scopes([/* ... */])
    ->renderHook(PanelsRenderHook::TOPBAR_END);
```

### Render the selector yourself

[](#render-the-selector-yourself)

Disable the built-in render hook and embed the Livewire component anywhere:

```
SubtenantScopingPlugin::make()
    ->scopes([/* ... */])
    ->withoutDropdown();
```

```
@livewire(\Leek\FilamentSubtenantScope\Livewire\SubtenantScopeSelector::class)
```

### Override the view

[](#override-the-view)

Publish and edit the dropdown blade:

```
php artisan vendor:publish --tag=filament-subtenant-scope-views
```

Multiple scopes
---------------

[](#multiple-scopes)

Stack as many as you need. Each gets its own dropdown and storage key:

```
SubtenantScopingPlugin::make()
    ->scopes([
        SubtenantScope::make('region', 'Region', Region::class, 'region_id'),
        SubtenantScope::make('location', 'Location', Location::class, 'location_id'),
    ]);
```

Resources can opt into one or both:

```
protected static array $subTenantScopes = [
    'region' => 'region_id',
    'location' => 'location_id',
];
```

Listening for changes
---------------------

[](#listening-for-changes)

The Livewire component dispatches `sub-scope-changed` after every selection (it also triggers a full page reload to refresh server-rendered scoped data):

```
Livewire.on('sub-scope-changed', ({ scopeKey, value }) => {
    // ...
});
```

Testing
-------

[](#testing)

```
composer test
```

How it works
------------

[](#how-it-works)

1. **Plugin** registers a render hook that pulls the dropdown into the topbar, scopes the manager request-singleton, and walks panel resources during `boot()` to attach Eloquent global scopes.
2. **Manager** resolves the active selection per scope (URL → session → user storage), caches per request.
3. **Trait** (`HasSubtenantScopes`) adds a panel-aware Eloquent global scope to the model. The scope is no-op outside the panel the plugin is registered on.
4. **Livewire selector** renders one dropdown per registered scope, writes the selection through the manager, and reloads the page so server-rendered data picks up the new filter.

License
-------

[](#license)

MIT. See [LICENSE.md](LICENSE.md).

###  Health Score

46

—

FairBetter than 92% of packages

Maintenance92

Actively maintained with recent releases

Popularity26

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity46

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

38d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/e11ad2f75cb1d558136f1982458d541d96fb84a217d6a82e8c18df2ff503964b?d=identicon)[leek](/maintainers/leek)

---

Top Contributors

[![leek](https://avatars.githubusercontent.com/u/60204?v=4)](https://github.com/leek "leek (4 commits)")

---

Tags

filamentphpfilamentphp-pluginlaravelmulti-tenancyfilamentfilamentphpscopetenancyleeksub-tenant

###  Code Quality

TestsPest

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/leek-filament-subtenant-scope/health.svg)

```
[![Health](https://phpackages.com/badges/leek-filament-subtenant-scope/health.svg)](https://phpackages.com/packages/leek-filament-subtenant-scope)
```

###  Alternatives

[dotswan/filament-map-picker

Easily pick and retrieve geo-coordinates using a map-based interface in your Filament applications.

127173.7k3](/packages/dotswan-filament-map-picker)[jibaymcs/filament-tour

Bring the power of DriverJs to your Filament panels and start a tour !

12351.0k](/packages/jibaymcs-filament-tour)[relaticle/custom-fields

User Defined Custom Fields for Laravel Filament

16345.8k](/packages/relaticle-custom-fields)[rawilk/profile-filament-plugin

Profile &amp; MFA starter kit for filament.

3913.7k](/packages/rawilk-profile-filament-plugin)[asosick/filament-layout-manager

Allow users to create &amp; customize their own FilamentPHP pages composed of Livewire components

5721.3k3](/packages/asosick-filament-layout-manager)[marcelweidum/filament-passkeys

Use passkeys in your filamentphp app

6643.3k](/packages/marcelweidum-filament-passkeys)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
