konekt/history
Konekt History is a Laravel package for tracking changes to Eloquent models. It records create, update and delete events, letting you audit modifications and browse past versions of your records with minimal setup.
composer require konekt/history. The package follows the standard Konekt layout (Contracts, Models, Traits, a ServiceProvider), so it will blend with the rest of the konekt/* ecosystem if you already use Concord.php artisan vendor:publish --tag=config and --tag=migrations (or publish by provider), then php artisan migrate. Expect a single polymorphic history-style table plus a config file controlling the table name, the user resolver, and what gets stored.HasHistory trait (Konekt\History\Traits\HasHistory) to the model you want audited. That's the only mandatory change — the trait hooks Eloquent's created/updated/deleted events.Order or Invoice model where you need "who changed what, when" for support and compliance. After saving a model, inspect $order->history (or $order->history()->latest()->get()) to see the recorded entries.Start by reading the trait first — it defines the entire public surface (event names, relation name, attribute recording). Then the config file, then the migration. The README is thin compared to the code.
save()/delete() writes one row containing the historiable morph, the event (created, updated, deleted), a JSON payload of changed attributes, and the acting user. This is the 90% path — attach the trait and forget it.$model->getDirty() / getChanges()), not the full record. Use diffs for storage economy; use full snapshots only if you need to reconstruct state without replaying the whole chain.$model->history()->where('event', 'updated')->latest()->paginate() is the canonical timeline query. Expose it through a policy-gated controller or an admin panel (Laravel Nova/Filament) rather than letting views hit the model directly.updated. Record them explicitly through the package's historian / recordHistory-style API so the timeline is complete, not just a diff log.auth()->user() via config; in queue jobs and console commands there is no user, so set a "system" actor or pass an explicit user ID to manual entries.tenant_id (or team_id) column to the history table and scope reads by it. The polymorphic relation does not know about tenancy by default.Order::where(...)->update([...]) and insert()/upsert() never fire model events, so no rows are recorded. Same for DB::table() raw updates and bulk imports. If those paths exist in your codebase, wrap them in manual history entries or refactor to save().updated_at-only saves produce noisy entries. A touch or a no-op save still fires updated. Filter out empty change sets in the trait override or in the writer so your timeline isn't 80% noise.updated, what you get is the post-save state of the dirty attributes; if you also need the "before" value, capture getOriginal() at updating time — the default updated hook gives you only the new values.SoftDeletes, a delete is an update; you'll see deleted events only if the trait is ordered after / coexists correctly with the soft-delete hooks. Verify this explicitly with a test — this is the most common silent bug in audit packages.->changes will come back as a string.php artisan config:cache, changing the table name or user resolver requires re-caching. Also, changing the table name after migrations means you must rename the table yourself.withoutHistory()-style bypass and turn it off in phpunit.xml / TestCase, enabling it only in the tests that assert history behavior.(historiable_type, historiable_id, created_at) and schedule an archival command (move >N months to cold storage) before it becomes the largest table in the DB.History contract/model in a service provider to add columns or custom casts; implement the package's historiable contract on models that can't use the trait (e.g. legacy models with custom save logic). Prefer overriding the writer service over editing vendor files.composer.json — there's no community pressure to keep BC.How can I help you explore Laravel packages today?