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: September 2026

When a staff member edits a client record, changes an order total, or deletes a document on a production Laravel application, you need more than updated timestamps—you need an audit trail. Laravel Activity Log with Spatie Package (spatie/laravel-activitylog) records model changes, custom business events, and the user who triggered them, stored in a dedicated database table you can query, filter, and display in an admin panel. On legal-tech portals and client portals I've maintained, that kind of accountability is not optional. This guide walks through installation, configuration, real-world patterns, and the production gotchas that bite teams after launch.

What does Laravel Activity Log with Spatie Package actually record?

The package persists each event as a row in an activity_log table. Every row captures four conceptual pieces: what happened (description and event name), who did it (the causer—usually an authenticated user), what was affected (the subject—typically an Eloquent model), and context (changed attributes stored as JSON in properties).

That separation matters when you build compliance-friendly admin screens. A law-firm client portal might show "Advocate Sharma updated Case #1042 status from draft to submitted" without exposing unrelated model fields. The same structure works on eCommerce platforms where finance teams ask who changed a voucher balance.

Activity Log Row StructureCauserUser / AdminSubjectEloquent ModelPropertiesJSON diffactivity_log tablelog_name, description, eventcauser_id, subject_type, subject_idproperties, batch_uuid, created_atQueryable audit trail for admin UI and API
Laravel Activity Log with Spatie Package stores causer, subject, and JSON properties in one queryable row

Common columns you will work with:

  • description — human-readable text such as "updated" or a custom string you set.
  • event — machine name like created, updated, or deleted.
  • subject_type / subject_id — polymorphic link to the affected model.
  • causer_type / causer_id — polymorphic link to the acting user.
  • properties — JSON with attributes, old, and any custom keys you add.
  • batch_uuid — groups related logs from one request (useful for multi-model workflows).

The package integrates cleanly with Laravel authorization because logging happens at the model layer, after your policies and Form Requests have already validated input. It does not replace authorization—it documents what passed through it.

How do you install and configure the Spatie activity log package?

Target Laravel 13.x on PHP 8.3 or higher for new projects; Laravel 12 on PHP 8.2 still runs the same package with equivalent setup. Install with Composer 2.10:

composer require spatie/laravel-activitylog

php artisan vendor:publish --provider="Spatie\Activitylog\ActivitylogServiceProvider" --tag="activitylog-migrations"
php artisan migrate

php artisan vendor:publish --provider="Spatie\Activitylog\ActivitylogServiceProvider" --tag="activitylog-config"

That creates config/activitylog.php and the migration for the activity_log table. The published config lets you set a default log name, enable or disable logging globally, and define how long you retain records.

Basic model setup

Add the trait and implement getActivitylogOptions() on any model you want audited:

<?php

namespace App\Models;

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

class Invoice extends Model
{
    use LogsActivity;

    protected $fillable = [
        'client_id',
        'amount',
        'status',
        'due_date',
    ];

    public function getActivitylogOptions(): LogOptions
    {
        return LogOptions::defaults()
            ->logOnly(['amount', 'status', 'due_date'])
            ->logOnlyDirty()
            ->dontSubmitEmptyLogs()
            ->useLogName('billing');
    }
}

Three options deserve emphasis:

  1. logOnly() — whitelist attributes. Never log passwords, API tokens, or encrypted payloads.
  2. logOnlyDirty() — skip rows where nothing meaningful changed (reduces noise).
  3. dontSubmitEmptyLogs() — suppress entries when dirty-check finds zero diffs.

On applications using Redis 8.10 for cache and queues, activity logging stays synchronous by default. That is fine for admin CRUD. For high-volume imports, queue the writes or batch them—covered later.

How do you log custom events beyond model saves?

Not every business action maps to an Eloquent update. Payment captured, document downloaded, case status manually overridden—these deserve explicit log entries. Spatie provides the activity() helper and the Activity facade.

use Spatie\Activitylog\Models\Activity;

activity('documents')
    ->causedBy(auth()->user())
    ->performedOn($document)
    ->withProperties([
        'ip' => request()->ip(),
        'action' => 'download',
        'file_size' => $document->size,
    ])
    ->event('downloaded')
    ->log('Client downloaded notarized PDF');

Chain methods in any order before ->log(). The performedOn() call sets the subject; causedBy() sets the causer. Custom properties land in the JSON column and are searchable if you add indexes or export to reporting tools.

Activity Logging FlowHTTP RequestControllerEloquentsave / deleteLogsActivitytrait hooksactivity_loginsert rowParallel path: activity() helperCustom events without model mutationAdmin audit UI / API endpointFilter by log_name, causer, date range
Automatic model hooks and manual activity() calls both write to the same activity_log table

For manual logs inside service classes—patterns I use in enterprise Laravel applications—keep descriptions consistent so filters work:

  • Use fixed log names: billing, documents, auth.
  • Use fixed event strings: downloaded, approved, archived.
  • Store machine-readable codes in properties, human text in description.

Which LogOptions settings should you use in production?

The LogOptions fluent API controls granularity. Pick settings deliberately—over-logging fills disks and slows admin queries; under-logging fails audits.

MethodWhat it doesWhen to use it
logAll()Logs every attribute on create/update/deleteSmall models with no secrets; rarely right for User models
logFillable()Logs only mass-assignable fieldsQuick start; verify fillable excludes sensitive columns
logOnly([...])Explicit attribute whitelistRecommended default for production audit trails
logExcept([...])Blacklist specific columnsWhen most fields are safe except a few
logOnlyDirty()Records only changed attributesAlmost always enable this
dontLogIfAttributesChangedOnly([...])Ignores noise like updated_atModels touched frequently by background jobs

Customising entries with tapActivity

Override tapActivity() on the model when you need to enrich or redact before insert:

public function tapActivity(Activity $activity, string $eventName): void
{
    $activity->description = "Invoice #{$this->id} {$eventName}";

    $props = $activity->properties->toArray();
    unset($props['attributes']['internal_notes']);
    $activity->properties = collect($props);
}

That pattern saved a client portal project from logging internal advocate notes into a client-visible audit export. Treat redaction as a first-class requirement, not an afterthought.

Disabling logging temporarily

Seeders, imports, and sync jobs should not pollute audit history:

use Spatie\Activitylog\Models\Activity;

Activity::disableLogging();

/* bulk update thousands of rows */

Activity::enableLogging();

Wrap imports in try/finally so logging re-enables even when an exception fires. I've debugged production tables bloated to millions of rows because a nightly sync forgot to disable logging.

How do you query, display, and retain activity logs?

The package ships an Activity Eloquent model. Query it like any other model, with relationships for causer and subject:

use Spatie\Activitylog\Models\Activity;

$logs = Activity::inLog('billing')
    ->causedBy($user)
    ->forSubject($invoice)
    ->latest()
    ->paginate(25);

Scopes such as inLog(), causedBy(), and forSubject() keep controller code readable. For JSON property search on MySQL 9.7 or PostgreSQL 18, use database JSON operators sparingly—index hot paths or mirror critical fields into dedicated columns if reporting demands it. See PostgreSQL for Laravel developers for JSON indexing trade-offs on larger deployments.

Automatic vs Manual LoggingLogsActivity traitModel create / update / deleteAttribute diffs automaticLess boilerplateBest for CRUD auditactivity() helperLogin, export, approveCustom propertiesExplicit descriptionsBest for business eventsBoth write to activity_log — use together
Combine LogsActivity for model CRUD with activity() for business events in Laravel Activity Log with Spatie Package

Retention and cleanup

The package includes an Artisan command to prune old records:

php artisan activitylog:clean --days=365

Schedule it in routes/console.php or your scheduler. For a portal with 50 staff and moderate CRUD, a year of logs often fits comfortably on a modest VPS. High-traffic eCommerce systems may need 90-day retention plus cold storage export. Document your retention policy—auditors ask.

Batch UUID for multi-step workflows

When one user action touches several models—approving a booking that updates inventory, payment, and notification records—assign a shared batch UUID so the admin UI groups them:

use Illuminate\Support\Str;
use Spatie\Activitylog\Facades\LogBatch;

LogBatch::startBatch();
$batchUuid = Str::uuid();

/* multiple model saves and activity() calls */

LogBatch::endBatch();

Readers of the log see one logical operation instead of scattered rows. That mirrors patterns discussed in event sourcing with Laravel Spatie packages, though activity log is an audit trail, not a full event store.

What production mistakes break Spatie activity logging?

Most failures I encounter are environmental or architectural, not package bugs.

Logging sensitive data

Never pass passwords, reset tokens, card numbers, or full KYC payloads into logOnly() lists. If a column might contain PII, exclude it. GDPR and local data-protection expectations apply even for Nepal-based businesses serving international clients. When debugging, use JSON formatter tools on exported log samples in staging—not production—to verify redaction.

Missing causer on queued jobs

When a job mutates models, auth()->user() is null. Pass the initiating user ID into the job and call activity()->causedBy($user) or set causer on the model before save using CauserResolver binding. Otherwise every automated change appears as "system" with no accountability.

Table growth without indexes

On MySQL 8.4 LTS or MySQL 9.7, ensure indexes exist on columns you filter—at minimum log_name, created_at, and polymorphic causer/subject keys. Slow admin audit pages get disabled by frustrated staff, which defeats the purpose.

Confusing activity log with versioning

Activity log records that a change happened and stores before/after snapshots in JSON. It does not give you point-in-time model reconstruction across arbitrary history the way a dedicated versioning package or event sourcing would. Choose the right tool; see CQRS pattern in Laravel when it helps for heavier audit requirements.

Production ChecklistExclude secrets from logOnlyRedact in tapActivitySchedule activitylog:cleanDefine retention policyPass causer into queue jobsAvoid anonymous system logsIndex log_name and datesKeep admin queries fastDisable during bulk importsActivity::disableLogging()Use batch UUID for workflowsGroup related entriesShip audit UI before you need it in a dispute
Production checklist for Laravel Activity Log with Spatie Package — security, retention, and query performance

Deploy through your normal pipeline—GitLab CI for Laravel with Deployer 7—and run migrations before symlink swap. The activity log table is append-heavy; plan backups accordingly alongside your main application database.

How does Spatie activity log compare to rolling your own?

Teams sometimes debate a custom audit_logs table versus a maintained package. Spatie wins on polymorphic causer/subject relationships, dirty detection, batch support, and battle-tested edge cases. A DIY table makes sense only when requirements are trivial—single model, no causer polymorphism, no JSON diffs.

Spatie sits alongside other packages I reach for regularly—Spatie Media Library for documents on portals like Mijar Law Associates, and Permission for RBAC. Together they form a practical admin foundation without bloating your codebase. Official docs live at Spatie's laravel-activitylog documentation; Laravel's Eloquent events are documented in the Laravel 13.x Eloquent guide.

For API-only apps exposing audit history, paginate aggressively and authorize with policies—never leak one client's logs to another. Patterns from Laravel API best practices and building RESTful APIs with Laravel apply directly.

Key Takeaways

  • Install spatie/laravel-activitylog, publish migrations and config, then add LogsActivity with an explicit logOnly() whitelist—not logAll() on models that touch sensitive data.
  • Enable logOnlyDirty() and dontSubmitEmptyLogs() on every production model to keep the activity_log table lean.
  • Use the activity() helper for business events (downloads, approvals, logins) that never touch Eloquent attributes.
  • Schedule activitylog:clean, index filter columns, and disable logging during bulk imports.
  • Pass the causer into queued jobs so automated changes remain attributable.
  • Build the admin audit UI early—Laravel Activity Log with Spatie Package only helps if staff can actually read the trail.

People Also Ask

Does Spatie activity log work with Laravel 13?

Yes. Install the current major release of spatie/laravel-activitylog on Laravel 13.x with PHP 8.3 or higher. Laravel 12 applications on PHP 8.2 use the same trait and configuration API. Run composer update spatie/laravel-activitylog during framework upgrades and re-run migrations if the package publishes new columns.

Can you log activity without an authenticated user?

Yes. Omit causedBy() and the causer columns stay null, or pass a system user model instance representing automated processes. For accountability, create a dedicated "System" user record rather than leaving causer empty on routine automated jobs you still want traceable.

How is activity log different from Laravel's built-in model events?

Eloquent created, updated, and deleted events fire hooks inside your application lifecycle. Activity log persists a durable database row with JSON diffs, causer, subject, and timestamps suitable for admin audit screens and compliance exports. Events are for code reactions; activity log is for human-readable history.

Not automatically across relationships. Log each model with its own trait, or use activity() on the parent after child saves, referencing both in withProperties(). Batch UUID grouping makes related entries easy to display together in one admin timeline.

Ship audit trails that survive the first compliance question

Laravel Activity Log with Spatie Package turns "who changed this?" from a database archaeology exercise into a query you run in seconds. Whitelist attributes, redact sensitive fields, schedule cleanup, and wire a simple admin view—before a client dispute or VAT audit makes it urgent. If you want activity logging integrated into a portal, booking system, or custom Laravel application with proper authorization and deployment, get in touch. For related Spatie tooling, read how to create custom Laravel packages, essential Laravel plugins, and modern Laravel architecture best practices—then inspect a shipped example on the Notary Nepal portfolio case study or explore ongoing Laravel support and maintenance if you already have an app that needs audit logging retrofitted.

Frequently Asked Questions

A Spatie Composer package that records Eloquent model changes and custom business events in an activity_log table, including who acted, what changed, and JSON context.

Run composer require spatie/laravel-activitylog with Composer 2.10 on Laravel 13.x with PHP 8.3 or higher, or Laravel 12 on PHP 8.2. Publish migrations with php artisan vendor:publish --provider="Spatie\Activitylog\ActivitylogServiceProvider" --tag="activitylog-migrations", run php artisan migrate, then publish config with the activitylog-config tag. That creates config/activitylog.php and the activity_log table. Add the LogsActivity trait and implement getActivitylogOptions() on each model you want audited.

Yes. Install the current spatie/laravel-activitylog release on Laravel 13.x with PHP 8.3 or higher; Laravel 12 on PHP 8.2 uses the same trait and config API.

Every entry captures four pieces: what happened via description and event names like created or updated; who did it through polymorphic causer_type and causer_id, usually an authenticated user; what was affected via subject_type and subject_id linking to the Eloquent model; and context in a properties JSON column holding changed attributes, old values, and custom keys. batch_uuid groups related logs from one request. On legal-tech portals I maintain, that structure lets admin screens show readable audit lines without exposing unrelated model fields.

Prefer logOnly() with an explicit attribute whitelist over logAll() or blind logFillable(). Enable logOnlyDirty() so only changed fields are stored, and dontSubmitEmptyLogs() to skip meaningless updates. Set useLogName() with fixed names like billing or documents so filters stay predictable. Use logExcept() only when most columns are safe. For models touched by background jobs, consider dontLogIfAttributesChangedOnly() to ignore noise like updated_at. On real client projects, this combination keeps the table lean while preserving audit value.

Use the activity() helper or Activity facade when the action is not a simple model save. Chain methods before log(): activity('documents')->causedBy(auth()->user())->performedOn($document)->withProperties(['ip' => request()->ip(), 'action' => 'download'])->event('downloaded')->log('Client downloaded notarized PDF'). Manual logs and automatic LogsActivity hooks write to the same activity_log table. In service classes, keep log names, event strings, and property keys consistent so admin filters and exports work reliably.

Use logOnly() with an explicit whitelist. Never log passwords, API tokens, reset tokens, card numbers, or full KYC payloads. Verify fillable arrays do not accidentally expose secrets if you use logFillable(). Override tapActivity() to redact fields such as internal_notes before insert. I have seen client-visible audit exports leak advocate notes because redaction was treated as an afterthought. GDPR and data-protection expectations apply even for Nepal businesses serving international clients, so treat exclusion lists as a first-class requirement.

Wrap bulk operations with Activity::disableLogging() before the work and Activity::enableLogging() after. Use try/finally so logging re-enables even when an exception fires during the import. I have debugged production tables bloated to millions of rows because a nightly sync forgot to disable logging. Seeders, spreadsheet imports, and sync jobs should never write thousands of audit rows that obscure real staff actions. Re-enable logging immediately after the batch completes so normal CRUD remains traceable.

Use the package Activity Eloquent model with built-in scopes. Example pattern: Activity::inLog('billing')->causedBy($user)->forSubject($invoice)->latest()->paginate(25). Scopes keep controller code readable. Eager-load causer and subject relationships to avoid N+1 queries on audit pages. For JSON property search on MySQL 9.7 or PostgreSQL 18, use database JSON operators sparingly and index hot filter paths. Build the admin audit UI early; the package only helps if staff can actually read the trail.

Schedule php artisan activitylog:clean --days=365 in your Laravel scheduler; high-traffic eCommerce may need 90-day retention plus cold storage export.

auth()->user() returns null inside queue workers because no HTTP session exists. Pass the initiating user ID into the job constructor, then call activity()->causedBy($user) on manual logs or bind the causer on the model before save using CauserResolver. Without that step, automated changes appear as system with no accountability, which breaks audit trust on client portals where finance or legal staff need to know who triggered a change versus what ran overnight.

Eloquent created, updated, and deleted events fire application hooks; activity log writes durable database rows with JSON diffs, causer, subject, and timestamps for admin audit screens.

No. Activity log records that a change happened and stores before and after snapshots in JSON properties, but it does not reconstruct arbitrary point-in-time model state across full history the way a dedicated versioning package or event sourcing approach would. Choose activity log for human-readable audit trails and compliance exports. When requirements demand replayable domain events or state reconstruction, look at heavier patterns such as CQRS or a dedicated versioning tool instead of stretching activity log beyond its design.

Use LogBatch::startBatch() before multiple model saves and activity() calls, then LogBatch::endBatch() when finished. The package assigns a shared batch_uuid so related rows group as one logical operation in the admin UI. That helps when approving a booking updates inventory, payment, and notification records in a single workflow. Without batch grouping, staff see scattered rows and miss the connection between changes that belonged to one business action.

Spatie wins on polymorphic causer and subject relationships, dirty-change detection, batch UUID support, and edge cases already handled in production. A DIY audit table makes sense only when requirements are trivial: single model, no causer polymorphism, no JSON diffs. On applications where I already use Spatie Permission and Media Library, activity log fits the same admin foundation without reinventing audit infrastructure. Official documentation lives at Spatie laravel-activitylog docs; for API exposure, paginate aggressively and authorize with policies so one client never sees another's logs.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: