bbs-lab/nova-action-event-columns
A Laravel Nova package that adds action event columns to your resources, making action and event data easier to display, scan, and review directly in Nova tables. Useful for quick visibility into recorded action activity and debugging workflows without writing custom column code.
1. Install and publish
composer require <vendor>/nova-action-event-columns # check README for the exact vendor/name
php artisan vendor:publish --provider="Vendor\NovaActionEventColumns\NovaActionEventColumnsServiceProvider"
php artisan migrate
You get three publishables — the migration, a custom-column migration stub, and a config file. Start by reading the config file; it is the entire surface area of the package.
2. Register the resource
Add the package's ActionResource to NovaServiceProvider::resources() (or Nova::resources([...])) so the action_events table becomes browsable in Nova with the extra columns rendered. Without this you have columns in the DB and nothing to look at.
3. First use case — turn on the IP column
Flip the ip_address toggle in the config, run any Nova action (a delete is the quickest), then open the action events resource. You should see request()->ip() recorded against the row. That single toggle is the fastest way to verify the package is wired correctly before you register anything custom.
Order matters: publish + migrate before enabling a column in config/registry. A registered column with no matching DB column will fail writes (see Gotchas).
Registering your own column. The registry is the package's main extension point — a name, a value resolver, and an optional Nova field factory:
// NovaServiceProvider::boot() (or a dedicated provider)
NovaActionEventColumns::column('tenant_id')
->resolve(fn () => tenant('id')) // runs on every write path
->field(fn () => BelongsTo::make('Tenant')->readOnly());
Omit ->field() and the resource falls back to a sensible read-only display field — fine for user_agent, trace_id, or other "just show me the value" columns. Provide a field factory when you want sortable(), filterable(), or a relationship.
Add the SQL column with the published stub. The stub exists so you don't hand-write an alter table. Publish it, name the column, migrate, then register the resolver. Keep custom columns nullable or give them a default.
Resolvers must be context-only, not model-only. The package fills columns from a creating hook on the ->save() paths and an insert() override on the mass-insert paths (deletes, force-deletes, restores, queued jobs, and every custom action). The insert() path has no model instance — so a resolver that reads $event->actionable will work for updates and explode on a bulk delete. Resolve from the request, the authenticated user, or cached app state instead:
->resolve(fn () => auth()->id()) // safe on both paths
->resolve(fn () => app(TenantContext::class)->id()) // safe on both paths
Queued and console actions. Delete/restore jobs run on the queue, where request() is a CLI request. ip_address will record 127.0.0.1 (or empty) rather than the user who queued the action. If the IP matters for audit purposes, capture it into the action's meta at dispatch time and resolve from there instead of relying on the built-in.
Bulk actions are N rows, N resolver calls. A bulk delete of 500 models calls your resolver 500 times. Keep resolvers cheap and memoise anything expensive at the top of the request (tenant lookup, feature flags) rather than querying per row.
Retention on a schedule. Add the prune command to your scheduler so the table doesn't grow unbounded:
Schedule::command('action-events:prune --days=90')->daily();
--hours and --all are there for aggressive cleanups; --all truncates and prompts in production.
Don't touch your actions. The value here is that existing Action classes, Resource::actions(), and Nova's built-in delete/restore flows need zero changes — columns are filled below the action layer. If you find yourself editing an action to populate an audit column, you've missed the point of the package.
insert() bypasses Eloquent events. If you have your own observers or ActionEvent::created listeners, they will not fire on delete/restore/bulk paths. Anything hanging off model events on action_events is unreliable now.action_events via DB::table('action_events')->insert(...) from its own code path, the override may not apply. Grep for action_events before you assume full coverage.--days=N against a scratch copy first and confirm the boundary — off-by-one on "90 days" is a support ticket either way. In scheduled/production contexts, make sure the confirmation prompt is satisfied non-interactively or the job silently hangs.save() or insert() — meta/changes populated means save(), a bare row with only IDs means the mass-insert path. Then (4) whether it ran on a queue worker and got a CLI request.How can I help you explore Laravel packages today?