Key takeaways
- ✓
spatie/laravel-activitylogwith theLogsActivitytrait 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/detachdon’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
| Package | Model | Filament integration |
|---|---|---|
| spatie/laravel-activitylog | Generic activity_log table with a subject, a causer and change data | Several plugins, or easy to build yourself |
| owen-it/laravel-auditing | audits table with old and new values, and auditor resolvers | tapp/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 migrateAlso 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, andrestoredwith 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 descriptionnamespace 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
ListActivitiesBySubjectandListActivitiesByCauserpages 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 loggedAlso 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
INSERTper change. That’s fine for admin workloads, but watch it on high-volume models. - Index
activity_logon(subject_type, subject_id)and(causer_type, causer_id)(the default migration does this) and oncreated_atif 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
- spatie/laravel-activitylog documentation
- spatie/laravel-activitylog on Packagist
- pxlrbt/filament-activity-log
- Laravel Eloquent events
- Filament relation managers
Related on The Web Tier: Filament actions and bulk actions · Roles and permissions in Filament
