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.
Package info
github.com/BBS-Lab/bbs-lab-nova-action-event-columns
pkg:composer/bbs-lab/nova-action-event-columns
Requires
- php: ^8.2
- illuminate/support: ^11.0 || ^12.0 || ^13.0
- laravel/nova: ^4.0 || ^5.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- nunomaduro/collision: ^8.0
- orchestra/testbench: ^9.0 || ^10.0 || ^11.0
- orchestra/workbench: ^9.0 || ^10.0 || ^11.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-arch: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- pestphp/pest-plugin-mutate: ^4.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
README
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 columns — tenant_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:
An event's detail page shows every column as a field — including the built-in IP:
Features
- 🌐 Built-in
ip_addresscolumn captured fromrequest()->ip(), toggleable via config - 🧩 Column registry — register your own columns with a value resolver and an optional Nova field
- 🔁 Every write path covered — a
creatinghook for->save()paths and aninsert()override for the mass-insert paths - 🖥️ Custom
ActionResourcethat surfaces your columns in Nova (your field, or a read-only default) - 🧹
action-events:prunecommand for retention (--days/--hours/--all) - 📦 Publishable migration, custom-column migration stub, and config
- 🧪 100% line coverage, PHPStan level 8, no
finalclasses, 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(ornulloutside an HTTP context — returnnulland 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.

