Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Audit Laravel Package

liberusoftware/audit

Audit logging for Laravel: automatically track model creates, updates and deletes, record the user and request context behind every change, and query a clean history of who changed what and when. Integrates with Eloquent and keeps an immutable trail for compliance and debugging.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a low-adoption (0 stars, no dependents) package, so treat the first hour as a reconnaissance exercise rather than following a blog post. The names below are the conventional shape for this kind of package — verify each against config/audit.php and the published migrations before wiring it into production.

1. Install and publish

composer require liberusoftware/audit
php artisan vendor:publish --provider="Liberu\Audit\AuditServiceProvider"

Publish both config and migrations. Audit packages almost always ship a migration you must own, because the table shape (UUID vs bigint, JSON vs text payload, tenant column presence) is environment-specific.

2. Read the config file first, in this order

Scan for these four keys before anything else — they define the package's whole personality:

  • actor resolver — how the "who" is determined (guard name, callback, or Auth::id())
  • tenant resolver — how the "which tenant" is determined; if you use stancl/tenancy or spatie/multitenancy, this is where you bolt it on
  • correlation — request ID / trace ID header name and propagation behaviour
  • retention — the prune window and whether pruning is a command, a job, or both

3. Migrate

php artisan migrate

Check the resulting table's indexes immediately (SHOW INDEX FROM audits). Audit tables live or die by their indexes; see Gotchas.

4. First use case — the one that earns the package its keep

Do not start with "audit everything." Start with the single table where a support ticket currently requires you to guess: subscriptions, invoices, or user role assignments. Attach the trait to that one model, make a change in tinker, and confirm you get a row with actor, tenant, request ID, and a before/after diff.

use Liberu\Audit\Concerns\Auditable;

class Subscription extends Model
{
    use Auditable;
}
$sub = Subscription::first();
$sub->update(['status' => 'cancelled']);

Audit::latest()->first();
// actor_id, tenant_id, correlation_id, event: updated,
// old: {status: active}, new: {status: cancelled}

If that row is correct end to end, the rest of the package is repetition. If the actor or tenant is null, stop and fix resolution — everything downstream depends on it.

Where to look first in the source: the trait (src/Concerns/Auditable.php), the resolver classes, and the retention command. Those three files are ~80% of the package's behaviour. Ignore the export layer until you need it.

Implementation Patterns

Pattern 1: Trait-based, model-scoped auditing (the default)

The trait is the happy path. Two knobs you will actually use:

class Invoice extends Model
{
    use Auditable;

    // Fields that should never land in the audit payload
    protected array $auditExclude = ['updated_at', 'last_viewed_at'];

    // Fields whose change is material enough to record
    protected array $auditInclude = ['status', 'total', 'currency', 'due_at'];

    // Human label for exports and the admin UI
    public function auditLabel(): string
    {
        return "Invoice #{$this->number}";
    }
}

Prefer auditInclude over auditExclude. An exclusion list is a permanent maintenance liability: every new column added to the model silently starts appearing in audits, including the PII column someone adds next quarter. An inclusion list fails closed.

Pattern 2: Explicit logging for non-model events

Model events cover CRUD. They do not cover "user downloaded the export," "admin impersonated a customer," or "webhook signature verification failed." Log those explicitly:

Audit::record('invoice.exported', $invoice)
    ->with(['format' => 'csv', 'row_count' => $count])
    ->by($request->user())
    ->commit();

The explicit path is also the escape hatch when you need a one-off audit inside a transaction that should not be rolled back — check whether the package writes synchronously on the same connection or defers to a job.

Pattern 3: Correlation across HTTP → queue → HTTP

This is the package's headline feature and the one most likely to be configured wrong. The chain is:

  1. Middleware reads X-Request-Id (or generates one) and stashes it in a container binding / context.
  2. Model changes during the request inherit it.
  3. A job is dispatched. The correlation ID must be serialised onto the job. Laravel's Context facade does this automatically in modern versions; if the package predates that, you will need a job middleware or a base job class that re-binds it in handle().
  4. A downstream job or a horizon worker makes another model change — that row must carry the same ID.

Test this explicitly:

public function test_correlation_survives_the_queue()
{
    Queue::fake();
    $this->withHeader('X-Request-Id', 'req-abc-123')
         ->post('/subscriptions/1/cancel');

    Queue::assertPushed(CancelSubscription::class, function ($job) {
        return $job->correlationId === 'req-abc-123';
    });
}

If correlation silently drops after step 2, the entire "trace a user action across services" value proposition collapses into a column of nulls.

Pattern 4: Tenant resolution, explicitly bound

Do not rely on the package's default tenant resolver unless your tenancy model matches its assumption exactly. Bind your own:

// AppServiceProvider::boot()
Audit::resolveTenantUsing(fn () => tenant()?->getKey());
Audit::resolveActorUsing(fn () => auth()->hasUser()
    ? auth()->user()->getKey()
    : null);

The actor resolver deserves a second look in multi-guard apps. Auth::id() returns the default guard's user; an admin acting through an admin guard will be recorded as anonymous unless you resolve explicitly.

Pattern 5: Retention as a scheduled, observable job

Retention boundaries are the reason this package exists as a package rather than ten lines in a trait. Wire it up deliberately:

// routes/console.php or Kernel::schedule()
Schedule::command('audit:prune')->dailyAt('03:15')->withoutOverlapping();

Two things to verify: (a) pruning is chunked, not a single DELETE ... WHERE created_at < ? that locks a 50M-row table, and (b) there's a dry-run flag so you can see what would be deleted before it is.

Pattern 6: Export boundary

Export is where audit packages typically go wrong by loading everything into memory. Check the signature — if it returns a collection, be suspicious on large tenants. The pattern you want:

Audit::forTenant($tenantId)
    ->between($from, $to)
    ->lazyById(1000)
    ->each(fn ($row) => $writer->write($row));

Write to a streamed CSV rather than ->get(), and record the export itself as an audit event (the auditor must be audited).

Pattern 7: Read-side integration

Audits are worthless if only a developer with tinker can read them. The fastest win is usually a relation on the parent model and a read-only Filament/Nova resource:

public function audits(): MorphMany
{
    return $this->morphMany(Audit::class, 'auditable')->latest('created_at');
}

Expose it on the detail screen of the model you picked in Getting Started. That single screen typically kills the "why did this change?" support ticket category.

Gotchas and Tips

Indexes are the whole ballgame. The default migration may only create (auditable_type, auditable_id). You will query by tenant_id, created_at, correlation_id, and actor_id within a week. Add composite indexes matching your actual read patterns, before the table is large:

INDEX idx_tenant_created (tenant_id, created_at),
INDEX idx_correlation (correlation_id),
INDEX idx_actor_created (actor_id, created_at)

Console and queue context have no actor. php artisan migrate, scheduler jobs, and queue workers run unauthenticated. Decide the convention now — either record actor_id = null with a source: console marker, or have jobs accept an explicit $actingUserId and re-bind it in handle(). Retrofitting this after six months of nulls is painful.

Beware the audit-of-audit loop. If any listener or observer touches the audits table itself, and the trait is applied broadly (or a base model), you can trigger recursion. Keep the audit model out of any trait/observer registration and never audit the audit table.

Bulk writes bypass the trait. Model::where(...)->update([...]), insert(), and raw DB::statement do not fire model events, so they produce no audit rows. This is the single most common "the audit log is missing entries" report. Either route bulk operations through explicit Audit::record() calls or forbid them in code review. Add a test that asserts a soft-delete and a forceDelete both log.

Dirty-attribute diffs and casts. Changes to cast attributes (datetime, array, enum) record cast values or raw values depending on implementation, and identical-looking timestamps can produce noisy "changed" entries due to precision. Verify with an enum column before trusting the diff. Also confirm the package ignores no-op saves (isDirty() empty) — if it doesn't, you'll accumulate rows from touch/update-timestamp churn.

Seeding and import performance. db:seed and CSV imports will generate thousands of audit rows against your resolvers. Disable auditing during imports (Audit::withoutAuditing(fn () => ...)) or you'll both slow the import and pollute the log with synthetic data that retention will later have to chew through.

Retention vs. legal hold. Retention prunes by age. The moment a customer disputes a charge or a legal hold lands, naive pruning destroys evidence. Either add an immutable flag/partition that the prune command respects, or confirm the package's retention boundary is configurable per tenant. Read the prune command source; don't assume.

Don't put secrets in the payload. auditExclude at the model level is necessary but insufficient — password hashes, API tokens, and card last-four all leak through $fillable defaults. Add a global redaction callback if the package offers one, and grep your first week of audit rows for anything that would fail a security review.

JSON column size and row bloat. Before/after snapshots of large models (long text, base64 attachments) will balloon the table. Consider recording only the included fields (Pattern 1) rather than the full model, and check whether the package stores full snapshots or diffs.

Adoption risk is the real gotcha. Zero stars, zero dependents, and a future-dated release means: no community answers when something breaks, no guarantee of a security patch, and an API that may shift. Mitigate by (a) pinning an exact version in composer.json, (b) writing integration tests for your resolver + correlation chain so an upgrade breaking them is caught, and (c) keeping the trait application thin — a wrapper service around Audit::record() means you can swap the package for a hand-rolled table if it's abandoned, without touching call sites.

Extension points worth knowing: the actor/tenant resolvers, the label formatter, and the prune command are the three seams the package exposes. Custom resolvers cover 90% of integration friction. If you need anything beyond those, subclass and bind in your own service provider rather than editing vendor code.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource