MVP Style Settings for Filament Multi-tenancy

Filament

Curious how to implement simple yet straightforward  settings in your Filament multi-tenancy application? Look no further. This article explains how to roll your own. Why roll your own? Well packages are great, but doing it in a simple and straightforward way feels more flexible and scalable to me.

Migration

First up is the migration. As you will see we're using simple `key/value` pairs in the database. Any setting may be `nullable` and we're using `text` as the type to accommodate all kinds of data.

`2025_06_19_070000_create_settings_table`:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('settings', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('tenant_id');
            $table->string('key');
            $table->text('value')->nullable();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('settings');
    }
};
lang-php

Model

Here is the `Setting.php` model which should be saved in `/app/Models`. It has a getter to retrieve a setting.

<?php

namespace App\Models;

use App\Traits\HasTenantRelationship;
use Filament\Facades\Filament;
use Illuminate\Database\Eloquent\Model;

class Setting extends Model
{
    use HasTenantRelationship;

    protected $fillable = [
        'tenant_id',
        'key',
        'value',
    ];

    public static function get(string $key, $tenantId = null): mixed
    {
        if (! $tenantId) {
            $tenantId = Filament::getTenant()->id;
        }

        $setting = static::where('tenant_id', $tenantId)
            ->where('key', $key)->first();

        return $setting?->value;
    }
}
lang-php

Trait

I use this handy trait in all my tenant-enabled models:

<?php

namespace App\Traits;

use App\Models\Tenant;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

trait HasTenantRelationship
{
    public function tenant(): BelongsTo
    {
        return $this->belongsTo(Tenant::class);
    }
}
lang-php

Page

Create a new Filament Page called `Settings.php` which will be used by the Filament Admin Panel UI. Save it in the `\app\Filament\Pages` directory. Create that directory if it doesn't exist already. This page goes hand in hand with the view, so after creating the page, create the view.

To make the UI shine, we're using tabs to group individual settings.

How to add new settings

When adding new settings, you will have to add it in three places:

1. Create a public variable with the name of the setting. In the code below we have `public $delete_notifications` and `public $another_setting`

2. Initialise the setting and it's default in the `$this->form->fill` array. Ensure these names match the public variables.

3. In the tabs schema, add your Filament components just like you normally would, e.g. a `Select` or a `TextInput` component as indicated in the example.

<?php

namespace App\Filament\Pages;

use App\Models\Setting;
use Filament\Facades\Filament;
use Filament\Forms\Components;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Form;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
/**
 * A settings page for the system.
 * 
 * @package App\Filament\Pages
 */
class Settings extends Page
{
    protected static ?string $navigationIcon = 'heroicon-o-cog-6-tooth';

    protected static string $view = 'filament.pages.settings';

    protected static ?string $navigationGroup = 'System';

    protected static ?int $navigationSort = 105;

    public $delete_notifications;
    public $another_setting;

    public function mount(): void
    {
        $settings = Setting::all();
        $map = [];

        foreach ($settings as $setting) {
            $map[$setting->key] = $setting->value;
        }

        $this->form->fill([
            'delete_notifications' => $map['delete_notifications'] ?? 'never',
            'another_setting' => $map['another_setting'] ?? null,
        ]);
    }

    public function form(Form $form): Form
    {
        return $form
            ->schema([
                Tabs::make('Label')->tabs([
                    'General' => $this->generalTab(),
                    'Extra' => $this->extraTab(),
                ]),
            ])->model(Setting::class);
    }

    protected function generalTab()
    {
        return Tabs\Tab::make('General')
            ->schema(
                [
                    Components\Select::make('delete_notifications')
                        ->options(
                            [
                                'daily' => 'Daily',
                                'weekly' => 'Weekly',
                                'monthly' => 'Monthly',
                                'never' => 'Never',
                            ]
                        )
                        ->hint('How frequently to delete notifications.'),

                ]);
    }

    protected function extraTab()
    {
         return Tabs\Tab::make('Extra')
             ->schema(
                 [
                     Components\TextInput::make('another_setting'),
                 ]);
    }

    public function create(): void
    {
        foreach ($this->form->getState() as $key => $value) {
            Setting::updateOrCreate(
                [
                    'tenant_id' => Filament::getTenant()->id,
                    'key' => $key,
                ],
                [
                    'value' => $value,
                ]
            );
        }

        Notification::make()
            ->title('Settings Saved')
            ->success()
            ->send();
    }

    // Optional if you are using Spatie Laravel Permission
    // public static function canAccess(): bool
    // {
    //     return auth()->user()->hasRole([
    //         Role::SuperAdmin->value,
    //     ]);
    // }
}
lang-php

If the names of the public variables do not match or you've forgotten to declare them, you will see this error:

No property found for validation: [name_of_setting]

View

The settings page refers to a view which should be saved in `/resources/views/filament/pages/settings.blade.php`.

It's a simple Filament panel view that has a form, a save button, and the modals as indicated in the manual:

<x-filament-panels::page>
    <form wire:submit="create">
        {{ $this->form }}

        <x-filament::button type="submit">
            Save
        </x-filament::button>
    </form>

    <x-filament-actions::modals />
</x-filament-panels::page>
lang-php

Example Use

$truncateEvents = Setting::get('truncate_events');
lang-php

That's it! In my opinion, although it might be some boilerplate to get started, it doesn't get easier going forward. The key/value pairs stored in the database makes the design flexible, and just having one page to maintain for new settings is fantastic. There's also no complex syntax to remember, just simple and straightforward Laravel and Filament magic.

Thanks so much to the Filament team for their wonderful work. Give me feedback about this article by pinging me here .

Alternatives

The Filament plugins page has quite a few existing settings plugins. If you have time to build and break then I recommend to try some of them.