Search

Audit Logging Every Model Change in Filament

Audit Logging Every Model Change in Filament

Key takeaways

  • ✓ spatie/laravel-activitylog with the LogsActivity trait records model create, update and delete events, including before and after values.
  • ✓ Show the history where people need it: a read-only History relation manager on each resource, plus one Audit log resource for admins.
  • ✓ “Every change” has gaps: query-builder mass updates, raw SQL and pivot attach/detach don’t fire model events. Filament bulk actions can bypass them too.
  • ✓ Version check: activitylog v5 requires PHP 8.4 and Laravel 12/13. v4 still serves older stacks. Match your Filament plugin to the version you use.

Also Read: Filament v3 to v5 Upgrade: PHP Skip v4 Without Breaking Your Panel

“Who changed this price?” “When did this customer’s email change?” “Did someone delete that invoice?” If your panel can’t answer these in seconds, you need an audit log. It also turns up as a hard requirement in SOC 2, ISO 27001 and GDPR accountability work. This guide sets one up for a Filament v5 panel, shows the history in the UI, and covers the gaps that make many audit logs less complete than their owners think.

Choosing a package

PackageModelFilament integration
spatie/laravel-activitylogGeneric activity_log table with a subject, a causer and change dataSeveral plugins, or easy to build yourself
owen-it/laravel-auditingaudits table with old and new values, and auditor resolverstapp/filament-auditing

Both are mature. We’ll use Spatie’s package here: it’s the most common in Filament projects and has the widest choice of plugins.

Version note (September 2026): activitylog v5 (5.1.x) requires PHP 8.4 and Laravel 12 or 13, and stores model changes in a dedicated attribute_changes column. If you’re on PHP 8.2 or 8.3, use v4. For the pxlrbt Filament plugin, 2.x pairs with activitylog 4 (Filament 4 and 5), and 3.x pairs with activitylog 5 (Filament 5).

Step 1: Install and log models

composer require spatie/laravel-activitylog
php artisan vendor:publish --provider="Spatie\Activitylog\ActivitylogServiceProvider" --tag="activitylog-migrations"
php artisan migrate

Also Read: Laravel and PHP

Add the trait to each model you want audited:

use Illuminate\Database\Eloquent\Model;
use Spatie\Activitylog\LogOptions;
use Spatie\Activitylog\Traits\LogsActivity;

class Invoice extends Model
{
    use LogsActivity;

    public function getActivitylogOptions(): LogOptions
    {
        return LogOptions::defaults()
            ->logFillable()                    // track mass-assignable attributes
            ->logExcept(['internal_notes'])    // never store these
            ->logOnlyDirty()                   // only what actually changed
            ->dontSubmitEmptyLogs()            // skip saves with no real changes
            ->useLogName('billing');
    }
}

Also Read: Laravel: Livewire 4 Islands

When an authenticated user edits an invoice through Filament, the package records:

  • the subject (the invoice),
  • the causer (the logged-in user),
  • the event (created, updated, deleted, and restored with soft deletes),
  • the old and new values of the changed attributes.

Filament saves records through Eloquent, so resource create, edit and delete pages are covered without any Filament-specific code.

Don’t log secrets

Use logExcept() for passwords, tokens, two-factor secrets and anything under GDPR special-category rules. Audit logs are usually kept longer than the data itself, so a secret in the log becomes a long-lived liability.

Step 2: A History relation manager on each resource

People want history next to the record, not in a separate screen. LogsActivity gives your model an activities relationship, so a relation manager works directly:

php artisan make:filament-relation-manager InvoiceResource activities description
namespace App\Filament\Resources\Invoices\RelationManagers;

use Filament\Infolists\Components\KeyValueEntry;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Actions\ViewAction;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;

class ActivitiesRelationManager extends RelationManager
{
    protected static string $relationship = 'activities';

    protected static ?string $title = 'History';

    public function isReadOnly(): bool
    {
        return true;
    }

    public function infolist(Schema $schema): Schema
    {
        return $schema->components([
            KeyValueEntry::make('properties.old')->label('Before'),
            KeyValueEntry::make('properties.attributes')->label('After'),
        ]);
    }

    public function table(Table $table): Table
    {
        return $table
            ->modifyQueryUsing(fn ($query) => $query->with('causer')->latest())
            ->columns([
                TextColumn::make('event')->badge(),
                TextColumn::make('causer.name')->label('By')->placeholder('System'),
                TextColumn::make('created_at')->since()->tooltip(fn ($record) => $record->created_at),
            ])
            ->recordActions([ViewAction::make()]);
    }
}

Also Read: Filament on Laravel 13: The Complete Compatibility Checklist

Register it in InvoiceResource::getRelations(). If you’re on activitylog v5, point the entries at the new attribute_changes column (for example attribute_changes.old and attribute_changes.attributes) instead of properties.*. Check the column in your migration.

Step 3: One audit log for admins

Security reviews need a cross-model view: “everything user X did last Tuesday”. Build a read-only resource over Spatie\Activitylog\Models\Activity:

public static function table(Table $table): Table
{
    return $table
        ->modifyQueryUsing(fn ($query) => $query->with(['causer', 'subject']))
        ->defaultSort('created_at', 'desc')
        ->columns([
            TextColumn::make('created_at')->dateTime()->sortable(),
            TextColumn::make('causer.name')->label('User')->searchable(),
            TextColumn::make('event')->badge(),
            TextColumn::make('subject_type')->label('Model')->formatStateUsing(fn ($state) => class_basename($state)),
            TextColumn::make('subject_id')->label('ID'),
            TextColumn::make('log_name')->toggleable(isToggledHiddenByDefault: true),
        ])
        ->filters([
            SelectFilter::make('event')->options(['created' => 'Created', 'updated' => 'Updated', 'deleted' => 'Deleted']),
            // causer is a morphTo, so provide options explicitly
            SelectFilter::make('causer_id')->label('User')
                ->options(fn () => User::query()->orderBy('name')->pluck('name', 'id'))
                ->searchable(),
            Filter::make('created_at')->schema([DatePicker::make('from'), DatePicker::make('until')])
                ->query(fn ($query, array $data) => $query
                    ->when($data['from'], fn ($q, $d) => $q->whereDate('created_at', '>=', $d))
                    ->when($data['until'], fn ($q, $d) => $q->whereDate('created_at', '<=', $d))),
        ]);
}

public static function canCreate(): bool
{
    return false;
}

Also Read: Build a Multi-Tenant SaaS Panel PHP with Filament v5 (Step by Step)

Also give the model a policy that allows only viewAny and view for auditors, so nobody can edit or delete audit entries from the panel. An audit log you can edit isn’t much of an audit log.

Plugin shortcuts

If you’d rather not build the UI, several plugins do it:

  • pxlrbt/filament-activity-log: adds ListActivitiesBySubject and ListActivitiesByCauser pages you register as routes ('activities' => ListInvoiceActivities::route('/{record}/activities')). 2.x: Filament 4/5 with activitylog 4. 3.x: Filament 5 with activitylog 5.
  • rmsramos/activitylog: a full activity-log resource and timeline.
  • AlizHarb/filament-activity-log: Filament 4/5, with timeline views, dashboard widgets and revert actions.

Check each plugin’s supported activitylog major against the one you installed. That’s the most common installation failure.

The gaps: what isn’t logged

Model-event logging is only as complete as your use of Eloquent models. Common holes:

1. Query-builder mass updates. This fires no model events, so it’s invisible to the log:

Invoice::whereIn('id', $ids)->update(['status' => 'void']);   // not logged

Also Read: Claude Opus 5.5 Released: Price Cut, Benchmarks & Breaking Changes for Developers

This is a real risk in Filament bulk actions. If you write $records->toQuery()->update(...) for speed, you lose the audit trail. Either iterate ($records->each->update(...), logged) or log a summary explicitly:

activity('billing')
    ->causedBy(auth()->user())
    ->withProperties(['ids' => $records->modelKeys(), 'status' => 'void'])
    ->event('bulk_updated')
    ->log('Voided ' . $records->count() . ' invoices');

2. Pivot changes. attach(), detach() and sync() on BelongsToMany, which covers role assignments in the Filament Select::relationship()->multiple() field, don’t fire model events on the parent. Log them in the page’s afterSave() hook, or use a custom pivot model with LogsActivity.

3. Queued jobs and commands. There’s no authenticated user, so the causer is null. Set it explicitly (activity()->causedBy($user)) or accept “System”.

4. Raw SQL and migrations. These aren’t logged. Keep data fixes in code-reviewed commands that log what they did.

5. Imports. Filament’s importer saves through Eloquent, so rows are logged, which can mean a very large volume. Consider disabling logging during imports (activity()->disableLogging() in beforeSave()) and writing one summary entry instead.

Performance and retention

  • Logging adds an INSERT per change. That’s fine for admin workloads, but watch it on high-volume models.
  • Index activity_log on (subject_type, subject_id) and (causer_type, causer_id) (the default migration does this) and on created_at if you filter by date.
  • Prune old entries on a schedule:

    // config/activitylog.php → 'delete_records_older_than_days' => 365
    Schedule::command('activitylog:clean')->daily();

Set retention with your compliance team. “Forever” is rarely the right answer under GDPR.

Test that auditing works

it('records who changed an invoice total', function () {
    $user = User::factory()->create();
    $invoice = Invoice::factory()->create(['total' => 100]);

    $this->actingAs($user);

    livewire(EditInvoice::class, ['record' => $invoice->getRouteKey()])
        ->fillForm(['total' => 150])
        ->call('save');

    $log = $invoice->activities()->latest()->first();

    expect($log->event)->toBe('updated')
        ->and($log->causer->is($user))->toBeTrue();
});

FAQ

How do I add an audit log to Filament?

Install spatie/laravel-activitylog, add LogsActivity and getActivitylogOptions() to your models, then display activities with a relation manager, a read-only resource, or a plugin such as pxlrbt/filament-activity-log.

Does Filament log changes automatically?

No. Filament has no audit log built in. It saves through Eloquent, though, so model-event-based packages capture resource changes without Filament-specific code.

Why are some changes missing from my activity log?

The most common causes are query-builder update() or delete() calls (including in bulk actions), pivot sync()/attach(), and raw SQL, none of which fire Eloquent model events.

Which activitylog version should I use?

Use v5 if you run PHP 8.4+ on Laravel 12 or 13. Otherwise use v4, and pick a Filament plugin version that matches it.

Sources and further reading

Related on The Web Tier: Filament actions and bulk actions · Roles and permissions in Filament

TWT Staff

TWT Staff

Writes about Programming, tech news, discuss programming topics for web developers (and Web designers), and talks about SEO tools and techniques

Your experience on this site will be improved by allowing cookies Cookie Policy