bbs-lab/nova-action-event-columns

Add extra columns to Nova's action_events table and fill them automatically on every action — client IP out of the box, plus your own columns via a resolver registry.

Maintainers

Package info

github.com/BBS-Lab/bbs-lab-nova-action-event-columns

Homepage

pkg:composer/bbs-lab/nova-action-event-columns

Transparency log

Statistics

Installs: 20

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-07-22 21:27 UTC

This package is auto-updated.

Last update: 2026-07-22 21:30:44 UTC


README

Latest Version on Packagist Tests Total Downloads

Add extra columns to Laravel Nova's action_events table and fill them automatically on every action. The client IP address (ip_address) ships as a built-in column; your app can register additional columnstenant_id, user_agent, anything — without forking, through a small resolver registry.

Nova writes action_events through two internal paths (event-firing ->save() and event-bypassing mass ::insert()), and only fires Eloquent events on one of them. This package hooks both, so your columns are populated no matter how the event is created: create, update, attach, delete, force-delete, restore, and every custom Nova action. Works on Nova 4 and Nova 5.

Screenshots

Split diagonally: light theme (top-left) / dark theme (bottom-right).

The auto-registered "Action Events" resource, with the built-in ip_address column filled on every event:

Action Events resource with the IP column

An event's detail page shows every column as a field — including the built-in IP:

Action event detail with the IP field

Features

  • 🌐 Built-in ip_address column captured from request()->ip(), toggleable via config
  • 🧩 Column registry — register your own columns with a value resolver and an optional Nova field
  • 🔁 Every write path covered — a creating hook for ->save() paths and an insert() override for the mass-insert paths
  • 🖥️ Custom ActionResource that surfaces your columns in Nova (your field, or a read-only default)
  • 🧹 action-events:prune command for retention (--days / --hours / --all)
  • 📦 Publishable migration, custom-column migration stub, and config
  • 🧪 100% line coverage, PHPStan level 8, no final classes, strict types everywhere

Requirements

  • PHP ^8.2
  • Laravel Nova ^4.0 || ^5.0
  • Laravel ^11.0 || ^12.0 || ^13.0

Both Nova majors are exercised in CI. Note that Nova 4 (through its inertiajs/inertia-laravel dependency) tops out at PHP 8.4 and Laravel 11; on PHP 8.5 or Laravel 12+, use Nova 5. Composer resolves the right combination for you.

Installation

Because Nova is a paid, private package, make sure your application is already authenticated against nova.laravel.com, then:

composer require bbs-lab/nova-action-event-columns

The service provider auto-registers via Laravel package discovery. The ip_address migration runs automatically. Publish the config, migration or custom-column stub if you want to tweak them:

# Config
php artisan vendor:publish --tag=nova-action-event-columns-config

# The shipped ip_address migration
php artisan vendor:publish --tag=nova-action-event-columns-migrations

# A stub for adding your own action_events column — edit it before migrating
php artisan vendor:publish --tag=nova-action-event-columns-stub

# Translations (en, fr) — to customise the resource / field labels
php artisan vendor:publish --tag=nova-action-event-columns-translations

php artisan migrate

Activation

Nova only records into a custom action_events model when you point its action resource at this package. In config/nova.php:

'actions' => [
    'resource' => \BBSLab\NovaActionEventColumns\Nova\ActionResource::class,
],

Then run php artisan migrate. Until this is set, Nova uses its own action resource and your extra columns stay null.

Usage

The built-in IP column

Once activated and migrated, every action event stores the request IP in ip_address — nothing else to do. Toggle it in config/nova-action-event-columns.php (env NOVA_ACTION_EVENT_COLUMNS_IP_ENABLED).

Registering custom columns

Register columns from a service provider's boot()in code, never in config, because closures cannot be serialized by config:cache. Each column gets a resolver (the value to store) and an optional field factory (how it shows in Nova):

use BBSLab\NovaActionEventColumns\Facades\NovaActionEventColumns;
use Laravel\Nova\Fields\Number;

public function boot(): void
{
    NovaActionEventColumns::register(
        'tenant_id',
        fn ($request) => $request?->user()?->tenant_id,       // value resolver
        fn () => Number::make('Tenant', 'tenant_id')->exceptOnForms(), // optional Nova field
    );
}
  • The resolver receives the current Illuminate\Http\Request (or null outside an HTTP context — return null and leave the column nullable).
  • When you omit the field factory, the column is shown read-only via Text::make(Str::headline($column), $column).
  • Existing values are never overwritten — resolvers only fill columns that are still null.

You must provision the database column yourself; publish the stub (--tag=nova-action-event-columns-stub), rename it, set the column name/type, and migrate.

Viewing the events in Nova

The package's Nova\ActionResource extends Nova's own and adds a field for each registered column — your registered field if you supplied one, otherwise the read-only default. The built-in ip_address ships as an "IP" field.

It is auto-registered as a navigable resource, so an "Action Events" entry appears in the Nova sidebar and you can browse every event with its columns — no Nova::resources() wiring required. Events also appear on a record's detail page (Nova's built-in action-event feed). To opt out of the navigation entry — to register your own resource, or to keep Nova's default where events show only on detail pages — set register_resource to false (env NOVA_ACTION_EVENT_COLUMNS_REGISTER_RESOURCE).

The action-events:prune command

Retention for the action_events table:

# Delete events older than 365 days (chunked)
php artisan action-events:prune --days=365

# Or by hours
php artisan action-events:prune --hours=48

# Wipe everything (TRUNCATE, resets the auto-increment)
php artisan action-events:prune --all

# Skip the production confirmation prompt
php artisan action-events:prune --days=365 --force

A window (--days / --hours) or --all is required — with neither, the command refuses. It is never auto-scheduled; wire it up yourself if you want it periodic:

// routes/console.php
Schedule::command('action-events:prune --days=365 --force')->daily();

TrustProxies caveat

request()->ip() reflects the real client only if your app trusts its proxies. Behind a load balancer, configure trusted proxies so the forwarded header is honoured:

->withMiddleware(function (Middleware $middleware) {
    $middleware->trustProxies(headers: Request::HEADER_X_FORWARDED_FOR);
})

If you trust proxies as *, the forwarded IP is client-spoofable — treat ip_address as a best-effort audit signal, not hard proof.

Testing

composer test            # Pest suite
composer test-coverage   # 100% line coverage on src/
composer analyse         # PHPStan level 8
composer format          # Pint (laravel preset + strict types)

A full embedded Nova app (via Orchestra Workbench) lets you exercise the flow in a real Nova instance:

composer serve   # boots Nova at http://localhost:8000/nova

Security

The built-in ip_address is captured on a best-effort basis and is only trustworthy when proxies are configured correctly (see the TrustProxies caveat). If you discover a security issue, please email paris@big-boss-studio.com instead of using the issue tracker.

Changelog

Please see CHANGELOG for what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Credits

License

The MIT License (MIT). Please see License File for more information.