
August 12, 2026
9 min read
Table of Contents
By Kokil Thapa | Last reviewed: August 2026
Auditing user actions and model changes is a non-negotiable requirement for legal-tech portals, eCommerce platforms, and any business-critical application handling sensitive data. Implementing Laravel Activity Log with Spatie Package gives you a structured, queryable audit trail without writing custom observers or event listeners from scratch. This guide covers the complete setup for Laravel 12 on PHP 8.2+, including configuration patterns I use daily on production client projects.
LogsActivity trait. It automatically records create, update, and delete events with customizable properties, causer resolution, and database storage optimized for high-traffic Laravel 12 applications.How do you install and configure Laravel Activity Log with Spatie Package?
The foundation of any reliable audit system is correct installation and sensible defaults. On a recent Laravel development project in Nepal, I needed to track document modifications across a legal portal where every change had compliance implications. The Spatie package handled this cleanly once configured properly.
Installation steps for Laravel 12
- Require the package via Composer (version 4.x supports Laravel 12 and PHP 8.2+):
composer require spatie/laravel-activitylog "^4.9" - Publish and run migrations to create the
activity_logtable:php artisan vendor:publish --provider="Spatie\Activitylog\ActivitylogServiceProvider" --tag="activitylog-migrations" php artisan migrate - Publish the configuration file to customize behavior:
php artisan vendor:publish --provider="Spatie\Activitylog\ActivitylogServiceProvider" --tag="activitylog-config"
The migration creates an indexed activity_log table with columns for log_name, description, subject_type, subject_id, causer_type, causer_id, properties (JSON), and timestamps. For applications expecting millions of rows, add composite indexes on (subject_type, subject_id) and (causer_type, causer_id) immediately after migration — queries filtering by subject or user will otherwise degrade as the table grows.
Critical configuration adjustments
The default config/activitylog.php works for prototyping but needs tuning for production. Three settings matter most:
default_log_name: Set this to your application's domain context (e.g.,'legal_portal','ecommerce') rather than the generic'default'. This enables log separation when multiple systems share a database.delete_records_older_than_days: Enable automatic pruning for compliance-bound applications. Setting this to365retains one year of history while preventing unbounded table growth. Pair this with Laravel's scheduler callingphp artisan activitylog:cleandaily.activity_model: Override this if you need custom accessors, relationships, or additional columns on the activity model itself. I've extended this on legal-tech projects to add case reference fields directly to the activity record.
How does the LogsActivity trait track model changes?
The LogsActivity trait hooks into Eloquent's model events and writes structured records to the activity_log table. Understanding what gets logged — and what doesn't — prevents both missing audit data and excessive noise.
Basic trait implementation
use Spatie\Activitylog\Traits\LogsActivity;
use Spatie\Activitylog\LogOptions;
class Document extends Model
{
use LogsActivity;
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->logOnly(['title', 'status', 'client_id', 'case_reference'])
->logOnlyDirty()
->dontSubmitEmptyLogs()
->setDescriptionForEvent(fn(string $eventName) => "Document {$eventName}");
}
} This configuration logs only specified attributes, records only changed values (logOnlyDirty()), skips empty updates, and generates human-readable descriptions. On legal-tech portals I maintain, this pattern captures exactly which fields changed during document revisions without storing entire model snapshots on every save.
Understanding what gets recorded
Each activity entry contains:
- Subject: The model instance being modified (polymorphic relation via
subject_typeandsubject_id) - Causer: The authenticated user triggering the change (resolved automatically from
auth()->user(); nullable for system-triggered events) - Description: Human-readable summary generated by your
setDescriptionForEventclosure - Properties: JSON column containing
attributes(new values) andold(previous values) whenlogOnlyDirty()is enabled - Log name: Category identifier for filtering and retention policies
A common mistake is assuming all model events are captured by default. The trait logs created, updated, and deleted events. Custom events like approved or archived require explicit manual logging via activity()->performedOn($model)->causedBy($user)->log('Document approved').
What are the best practices for logging sensitive data and performance optimization?
Audit logs become liabilities when they expose PII, credentials, or payment data — or when they grow so large that queries timeout. Both problems are preventable with deliberate configuration.
Excluding sensitive attributes
Never log passwords, tokens, credit card numbers, or national ID fields. Use logExcept() instead of logOnly() when most attributes are safe but a few are dangerous:
return LogOptions::defaults()
->logExcept(['password', 'secret_token', 'national_id_number', 'bank_account'])
->logOnlyDirty(); On a Nepal-based legal portal handling marriage registration documents, we explicitly excluded national ID numbers and witness contact details from activity logs while retaining all case metadata. This satisfied audit requirements without creating a secondary PII store that would complicate GDPR-equivalent compliance.
Performance considerations at scale
The activity_log table becomes a bottleneck on high-write applications. Three mitigations work reliably in production:
- Partition by date: For tables exceeding 10 million rows, partition by month. Queries filtering by date range then scan only relevant partitions.
- Async logging via queues: Configure
'queue' => truein the config file to dispatch activity creation as a queued job. This removes write latency from the request cycle. Ensure your queue worker processes theactivitylogqueue with adequate concurrency. - Selective logging: Not every model needs auditing. Apply
LogsActivityonly to entities with genuine compliance, debugging, or business-audit requirements. Logging every product view or cart update creates noise and storage costs without proportional value.
How do you query and display activity logs efficiently?
Recording activities is half the problem; retrieving them without N+1 queries or full-table scans is the other. The package provides a fluent query builder that respects the indexes created during setup.
Common query patterns
// All activities for a specific document
$activities = Activity::forSubject($document)
->latest()
->paginate(25);
// Activities by a specific user in the last 30 days
$activities = Activity::causedBy($user)
->where('created_at', '>=', now()->subDays(30))
->with('subject')
->latest()
->get();
// Filter by log name and event type
$activities = Activity::inLog('legal_portal')
->where('event', 'updated')
->where('properties->>$.attributes.status', 'approved')
->latest()
->limit(100)
->get(); Always eager-load the subject and causer relationships when displaying activity lists. Without with(['subject', 'causer']), each row triggers two additional queries. On a dashboard showing the last 50 activities, this reduces queries from 101 to 3.
Displaying in Blade templates
<table class="table">
<thead>
<tr>
<th>Time</th>
<th>User</th>
<th>Action</th>
<th>Changes</th>
</tr>
</thead>
<tbody>
@foreach($activities as $activity)
<tr>
<td>{{ $activity->created_at->diffForHumans() }}</td>
<td>{{ $activity->causer?->name ?? 'System' }}</td>
<td>{{ $activity->description }}</td>
<td>
@if($activity->properties->has('old'))
@foreach($activity->changes['attributes'] as $field => $new)
<span class="badge bg-secondary">{{ $field }}</span>
{{ $activity->changes['old'][$field] ?? 'null' }} → {{ $new }}<br>
@endforeach
@endif
</td>
</tr>
@endforeach
</tbody>
</table> For legal-tech clients who need exportable audit reports, combine this with Laravel API best practices to expose filtered activity endpoints consumed by PDF generation services or external compliance dashboards.
| Query Scenario | Recommended Method | Index Required | Pagination? |
|---|---|---|---|
| All activities for one model | forSubject($model) | (subject_type, subject_id) | Yes, always |
| User's recent actions | causedBy($user)->latest() | (causer_type, causer_id, created_at) | Yes |
| Filter by field change | JSON path query on properties | Generated column + index (MySQL 8.0+) | Yes, limit results |
| Export / bulk retrieval | Chunked cursor pagination | created_at index | Cursor, not offset |
How does Laravel Activity Log with Spatie Package compare to alternatives?
Before committing to any audit solution, understand what you're trading off. I've evaluated several approaches on custom Laravel admin panels and client portals over the years.
Spatie's package wins for most Laravel projects because it integrates natively with Eloquent, requires zero external services, and provides enough flexibility to handle everything from simple CRUD logging to complex compliance workflows. Custom observers give more control but multiply maintenance burden. Database triggers are opaque to application code and difficult to test. Dedicated audit SaaS platforms add cost and latency that rarely justify themselves for Nepal-based SMBs or mid-market applications.
If you're building a Laravel application for a Nepali business with moderate traffic and clear audit requirements, Spatie Activity Log is the pragmatic default. Reserve custom solutions for cases where you need sub-millisecond write overhead, cross-database correlation, or regulatory frameworks that mandate tamper-proof storage outside your application database.
Implementing Laravel Activity Log with Spatie Package in Production
Getting Laravel Activity Log with Spatie Package working correctly in production means treating it as infrastructure, not an afterthought. Install with intention, configure for your domain's sensitivity and scale, exclude PII defensively, enable queue-based logging before traffic demands it, and build retrieval queries that respect your indexes. The package has been stable across Laravel 11 and 12 on PHP 8.2 through 8.4, and its conventions align with how experienced Laravel developers already structure model concerns.
If you're planning an audit implementation for a legal portal, eCommerce platform, or multi-tenant SaaS and want to avoid the pitfalls I've documented here, reach out to discuss your specific requirements. Correct audit architecture is cheaper than retrofitting compliance after launch.


