Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Laravel Activity Log with Spatie Package

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.

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

  1. Require the package via Composer (version 4.x supports Laravel 12 and PHP 8.2+):
    composer require spatie/laravel-activitylog "^4.9"
  2. Publish and run migrations to create the activity_log table:
    php artisan vendor:publish --provider="Spatie\Activitylog\ActivitylogServiceProvider" --tag="activitylog-migrations"
    php artisan migrate
  3. 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 to 365 retains one year of history while preventing unbounded table growth. Pair this with Laravel's scheduler calling php artisan activitylog:clean daily.
  • 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.
Composer Installspatie/laravel-activitylog ^4.9Run MigrationsCreate activity_log tablePublish ConfigCustomize defaultsApply TraitLogsActivity on modelsKey Configuration Settings• default_log_name: Domain-specific identifier (legal_portal, ecommerce)• delete_records_older_than_days: Enable auto-pruning (365 recommended)• activity_model: Extend for custom fields or relationships• Add composite indexes on (subject_type, subject_id) and (causer_type, causer_id)
Laravel Activity Log with Spatie Package installation sequence and critical production configuration settings

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_type and subject_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 setDescriptionForEvent closure
  • Properties: JSON column containing attributes (new values) and old (previous values) when logOnlyDirty() 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:

  1. Partition by date: For tables exceeding 10 million rows, partition by month. Queries filtering by date range then scan only relevant partitions.
  2. Async logging via queues: Configure 'queue' => true in the config file to dispatch activity creation as a queued job. This removes write latency from the request cycle. Ensure your queue worker processes the activitylog queue with adequate concurrency.
  3. Selective logging: Not every model needs auditing. Apply LogsActivity only 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.
User RequestEloquent SaveSync LoggingBlocks response +20-50msQueue DispatchReturns immediatelyDirect DB WriteRedis QueueWorker writes asyncWhen to Use Each Approach✗ Sync: Low-traffic admin panels, real-time audit display required, simple deployments✓ Queue: High-traffic apps, API endpoints, checkout flows, >100 writes/min sustained⚠ Always monitor queue lag — stale activity logs defeat the purpose of auditing
Synchronous versus queue-based activity logging trade-offs for Laravel Activity Log with Spatie Package in production

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 ScenarioRecommended MethodIndex RequiredPagination?
All activities for one modelforSubject($model)(subject_type, subject_id)Yes, always
User's recent actionscausedBy($user)->latest()(causer_type, causer_id, created_at)Yes
Filter by field changeJSON path query on propertiesGenerated column + index (MySQL 8.0+)Yes, limit results
Export / bulk retrievalChunked cursor paginationcreated_at indexCursor, 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.

Need Audit Trail?Laravel app + PHP 8.2+ + MySQL/PostgreSQL?YesNoSpatie Activity LogBest balance of features + controlExternal Audit SaaS / DB TriggersNon-PHP stack or compliance mandateChoose Spatie When:• Need model-level granularity• Budget-sensitive (no SaaS fees)• Want full query control + customization• Team knows Laravel ecosystem well
Decision framework for selecting Laravel Activity Log with Spatie Package versus alternative audit solutions

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.

Frequently Asked Questions

A Composer package that automatically records model changes, user actions, and custom events into a database table for auditing and debugging in Laravel applications.

The package is free and open-source under the MIT license, costing zero NPR or USD for commercial or personal use in any Laravel project.

Version 4.x supports Laravel 10, 11, and 12 with PHP 8.2 or higher, including the current PHP 8.4 stable release.

Run composer require spatie/laravel-activitylog, publish the migration with php artisan vendor:publish --provider="Spatie\Activitylog\ActivitylogServiceProvider" --tag="activitylog-migrations", then migrate. Add the LogsActivity trait to any Eloquent model you want tracked. Configure default options in config/activitylog.php after publishing the config file. In my experience deploying this on legal-tech portals like Court Marriage In Nepal, always verify the activity_log table exists before enabling traits in production to avoid deployment failures during zero-downtime releases.

Use the activity() helper function anywhere in your codebase. Call activity()->performedOn($model)->causedBy($user)->withProperties(['key' => 'value'])->log('Description'). This works in controllers, jobs, listeners, or services. I have used this pattern extensively in booking systems like Adventure Third Pole Trek to record non-model events such as payment confirmations and itinerary status changes that do not map directly to Eloquent create/update/delete operations but still require full audit trails for client accountability.

Yes. Define $logAttributes on your model using the LogsActivity trait. Set it to an array of column names like ['status', 'amount'] instead of the default wildcard. You can also use $logOnlyDirty = true to record only attributes that actually changed. This reduces storage overhead significantly on high-traffic tables. On eCommerce projects like Nepal Gift Card, I restrict logging to order status and payment fields only, ignoring updated_at timestamps and internal metadata that generate noise without audit value.

The package writes synchronously by default, which adds latency to every tracked request. Enable asynchronous logging via queueable activities in config/activitylog.php or implement a custom activity logger that dispatches jobs. Add composite indexes on subject_id, causer_id, and created_at columns for query performance. In production systems I maintain, synchronous logging on tables exceeding one million rows causes noticeable response degradation. Always benchmark before enabling on high-write models and consider partitioning or archival strategies for logs older than ninety days.

Yes. The package automatically resolves the authenticated user via Auth::guard(), which includes Sanctum token guards when configured correctly. Ensure your Sanctum guard is set as default or explicitly specify it in config/activitylog.php under default_auth_driver. API requests authenticated via Bearer tokens will correctly populate the causer_id field. I have implemented this on REST APIs serving mobile clients where tracking which API consumer modified resources was critical for debugging integration issues and resolving disputes between third-party developers and backend teams.

Use the Activity model provided by the package. Chain queries like Activity::forSubject($model)->causedBy($user)->latest()->paginate(50). Always add database indexes on subject_type, subject_id, causer_id, and created_at columns. Avoid loading all logs without pagination. For reporting dashboards on platforms like Mijar Law Associates, I create dedicated read-only endpoints with strict date-range filters and eager-loaded relationships. Raw SQL aggregation queries outperform Eloquent for statistics. Never expose unfiltered activity logs to end users without authorization checks, as they often contain sensitive operational data.

Yes. Use the withProperties() method when logging manually or define $logAttributesAsJson on models using the trait. Properties are stored as JSON in the properties column. Include request IP, user agent, or business context like invoice numbers. On legal service portals like Notary Nepal, I attach document reference IDs and case numbers to every activity so support staff can trace exactly which file version triggered a status change without cross-referencing multiple tables. Keep property payloads lean to avoid bloating the activity_log table over time.

Activity logs often contain sensitive user actions and must be protected. Create a dedicated Policy for the Activity model and enforce it in controllers and API resources. Never expose raw activity endpoints without middleware checks. Restrict admin panel access to specific roles using Spatie Permission alongside Activity Log. On client portals I build, only super-admins and compliance officers can view full audit trails. Regular users see only their own activities. Always encrypt PII in properties if required by local regulations and audit who accessed the logs themselves.

Missing migrations after upgrades cause immediate failures; always run migrate after updating. Null causer_id occurs when system processes trigger logging outside authenticated contexts; handle gracefully with nullable checks. Stale config caches after changing log attributes require php artisan config:clear. Trait conflicts arise when combining with other packages modifying model events; check event listener priority. In Deployer 7 workflows I use, forgetting to clear opcache after deploying config changes leads to inconsistent logging behavior until PHP-FPM reloads. Test logging in staging before production deploys.

Telescope is a development debugging tool, not a persistent audit system; its data is ephemeral and unsuitable for compliance. Native Laravel logging writes to files, making structured querying impossible. Spatie Activity Log provides permanent, queryable, relational audit records tied to models and users. Use Telescope for local debugging and Activity Log for production auditing. On regulated platforms like legal-tech portals, file-based logs fail compliance requirements because they cannot prove tamper resistance or support efficient forensic searches across years of operational history.

Yes. Unbounded growth degrades query performance and increases backup sizes. Implement a scheduled command using Prunable trait or manual chunked deletion for records older than your retention policy. Archive to cold storage if compliance requires long-term retention. On high-volume eCommerce systems like Quick And Easy Nepalese Grocery, I prune completed order activities after eighteen months while retaining payment-related logs indefinitely. Always test archival scripts in staging first. Consider partitioning the activity_log table by month for easier maintenance on systems exceeding five million records.

Yes. It pairs naturally with Spatie Laravel Permission. Use role and permission checks to control who can view, export, or manage activity logs. Log permission changes themselves by adding the trait to Permission and Role models. In multi-tenant applications I have built, activity visibility is scoped by tenant ID stored in properties, ensuring vendors on marketplaces like Ajako Deal only see their own operational history. Combine with model policies for granular access control rather than relying solely on global admin flags, which become unmanageable as organizational complexity grows.

Share this article

Quick Contact Options
Choose how you want to connect me: