Filament: Persist Toggleable Column Preferences to the Database

2026-04-08 Filament v4/5

When you make table columns toggleable, Filament saves the user's visibility choices in the session. That works fine until they log out, switch devices, or the session expires. Then everything resets to defaults.

If your users customize their table views and expect those choices to stick around, you need to persist them somewhere more permanent. Here's how to save column preferences to the database instead.


How Session Persistence Works

Under the hood, Filament's HasColumnManager trait on the Livewire component has two methods that handle persistence:

  • loadTableColumnsFromSession() reads column state from the session
  • persistTableColumns() writes column state to the session

The data itself is a simple array of column names, visibility flags, and ordering. We just need to redirect that storage to a database table.


Create the Migration

First, a table to store preferences per user and per Filament page:

php artisan make:migration create_user_table_preferences_table

database/migrations/xxxx_xx_xx_create_user_table_preferences_table.php

Schema::create('user_table_preferences', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('table_key');
$table->json('columns');
$table->timestamps();
 
$table->unique(['user_id', 'table_key']);
});

The table_key will hold the session key Filament already generates (an MD5 hash of the Livewire component class), so each resource page gets its own preference row.


Create the Model

php artisan make:model UserTablePreference

app/Models/UserTablePreference.php

<?php
 
namespace App\Models;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
class UserTablePreference extends Model
{
protected $fillable = [
'user_id',
'table_key',
'columns',
];
 
protected function casts(): array
{
return [
'columns' => 'array',
];
}
 
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

Create a Trait to Override Persistence

Instead of modifying every List page individually, we'll create a reusable trait:

app/Filament/Concerns/PersistsColumnsInDatabase.php

<?php
 
namespace App\Filament\Concerns;
 
use App\Models\UserTablePreference;
 
trait PersistsColumnsInDatabase
{
protected function loadTableColumnsFromSession(): array
{
$preference = UserTablePreference::query()
->where('user_id', auth()->id())
->where('table_key', $this->getTableColumnsSessionKey())
->first();
 
return $preference?->columns ?? $this->getDefaultTableColumnState();
}
 
protected function persistTableColumns(): void
{
if (! $this->getTable()->persistsColumnsInSession()) {
return;
}
 
UserTablePreference::query()->updateOrCreate(
[
'user_id' => auth()->id(),
'table_key' => $this->getTableColumnsSessionKey(),
],
[
'columns' => $this->tableColumns,
],
);
}
}

Two things to note:

  1. loadTableColumnsFromSession() falls back to getDefaultTableColumnState() when no database record exists, same as the original session logic.
  2. persistTableColumns() still respects the persistsColumnsInSession() flag. If you've disabled persistence on a specific table, this won't write to the database either.

Use the Trait on a List Page

app/Filament/Resources/Orders/Pages/ListOrders.php

<?php
 
namespace App\Filament\Resources\Orders\Pages;
 
use App\Filament\Concerns\PersistsColumnsInDatabase;
use App\Filament\Resources\Orders\OrderResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
 
class ListOrders extends ListRecords
{
use PersistsColumnsInDatabase;
 
protected static string $resource = OrderResource::class;
 
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

That's it. Column visibility and ordering preferences now survive logouts, session expiry, and device switches.


Apply It Globally

If you want this on every List page without adding the trait one by one, you can create a base class:

app/Filament/Pages/BaseListRecords.php

<?php
 
namespace App\Filament\Pages;
 
use App\Filament\Concerns\PersistsColumnsInDatabase;
use Filament\Resources\Pages\ListRecords;
 
class BaseListRecords extends ListRecords
{
use PersistsColumnsInDatabase;
}

Then extend BaseListRecords instead of ListRecords in your resource pages.


The getTableColumnsSessionKey() method generates a unique key per Livewire component class, so you don't need to worry about collisions between different resources. If you also use reorderableColumns(), the ordering is stored in the same columns JSON, so it gets persisted to the database too.

A few of our Premium Examples: