Product Decisions This Supports
Feature-level bets this unblocks (2–4 sprint-weeks of engineering avoided per surface):
- Revision history / "who changed what" panels in any CRUD-heavy admin: CMS pages, product copy, policy and T&C documents, pricing tables, internal wikis, marketing landing pages. Ship
$post->versions + diff() (array, text, HTML, side-by-side HTML) as the read model for a history drawer instead of building a bespoke audit table per entity.
- Undo / rollback as a user-facing capability.
revertToVersion(), revertWithoutSaving() (for "preview before restore" flows), and isLatest() map directly to a one-click "Restore this version" button with a confirmation preview — a feature users ask for but that rarely justifies its own epic.
- Draft → publish → revert workflows for content products, where "revert" is the customer's own recovery path and therefore a support-ticket reducer, not just a nice-to-have.
- Change-diff visualisation in review/approval flows (editor submits, reviewer sees highlighted deltas). Rendering is delegated to
jfcherng/php-diff, so we inherit mature diff formats rather than inventing a comparison UI.
- Safe data operations:
withoutVersion() lets us run backfills, imports, and one-off corrections without polluting customer-visible history — this is the single most important guardrail for whether history stays trustworthy after an ops incident.
Build vs. buy:
- Buy (adopt). The core mechanics — attribute-level diff vs. full snapshot strategy, retention cap, whitelist/blacklist of versionable columns, soft-delete/restore of versions, custom version model, per-model strategy — are commodity. Rebuilding them well is a 1–3 week effort plus ongoing edge-case maintenance (global scopes, connection handling, nested disable calls — all visible in this project's recent patch history).
- Build only if we need actor attribution, tamper evidence, or cross-entity transactional snapshots as first-class primitives. Here they are extensions, not defaults.
Roadmap signals to weigh:
- Versioning is a platform primitive, not a feature. Once adopted for one model, it sets the expectation ("history exists everywhere") and becomes a standard column on the platform roadmap. Decide deliberately whether history is a product promise or an internal convenience — that decision, not the package, drives storage and retention roadmap items.
- Storage and write amplification become roadmap items. Every save of a versioned model writes a version row. Retention policy (configured cap), archival of old versions, and eventual partitioning of the versions table belong on the platform/infra roadmap before the third or fourth high-write model is versioned.
- Do not schedule compliance-grade audit logging, legal hold, e-signature trails, or collaborative merge/branching onto this package. Those are separate products; if the roadmap demands them, this is the wrong primitive and we should scope a dedicated audit service.
Explicit non-goals to record in the decision log: no actor/user attribution out of the box, no immutable or tamper-evident log, no per-field access control on historical values, no branch/merge, no async version writes. All are extensible ($versionModel, custom columns) but each is ours to build and own.
When to Consider This Package
Adopt when all of these hold:
- The stack is Laravel 9+ / PHP 8.1+, and we are comfortable that the current major line targets PHP 8.3+ and Laravel 13 — i.e. our upgrade cadence tracks Laravel releases rather than lagging them.
- Versioning needs are per-model attribute history: text/content columns, status fields, configuration values. The versionable whitelist/blacklist lets us keep noise (timestamps, counters, caches) out of history.
- A "revert to version N" or "show me what changed" capability is the actual user need — not a formal audit obligation.
- We can absorb the operational cost: a versions table per database, retention capped by config, and version writes inside the same request as the model save. Acceptable for content-editing traffic; questionable for high-throughput ingestion.
- We accept single-maintainer risk (overtrue) and mitigate it: pin the version, lock dependencies, keep an integration test around our versioned models, and treat the trait surface as a thin wrapper we could reimplement if abandoned.
- The ecosystem fit is right: MIT, no license cost, small dependency surface, an existing Filament integration (mansoorkhan96/filament-versionable) if our admin runs on Filament, and a documented customization path via
$versionModel.
Look elsewhere when any of these hold:
- Compliance, regulated, or contractual audit needs — SOC 2 / HIPAA / financial or legal record retention, legal hold, "prove this record was not altered." You need append-only, tamper-evident, actor-attributed logging; this package is a history feature, not an audit system.
- You need relationship/graph versioning — versions of a parent and its children as one atomic unit, or snapshotting across aggregates. This versions model attributes, not object graphs.
- Collaborative editing — real-time co-editing, merge, conflict resolution, branches. Out of scope by design.
- High-volume or high-contention writes where an extra insert per save materially affects throughput or lock behaviour, and there is no appetite for queuing/deferring version writes.
- Multi-stack estates where the same history model must serve non-Laravel services — a shared audit/history service is the better centre of gravity.
- Weak signal tolerance: the project shows modest public adoption (≈586 stars, no listed dependents) and a single primary maintainer. That is fine for an internal tool with tests and a pin; it is not fine as an unexamined dependency on a revenue-critical, unrecoverable path without a fallback plan.
Adoption gate (definition of done before rollout): pinned version, one integration test per versioned model covering create/update/revert, a documented retention setting, and a written answer to "who is allowed to revert, and is that permission checked?" — the package does not answer that for you.
How to Pitch It (Stakeholders)
For executives (30 seconds): We can ship user-visible "revision history" and "restore this version" — the capability customers associate with mature content and admin products — by adding one well-scoped, MIT-licensed open-source library rather than a multi-week build. No licence cost, small footprint, active maintenance with recent Laravel 13 support. The trade-off is honest and bounded: it gives us history and diffs, not a compliance-grade audit trail, and it is a small single-maintainer project, so we pin the version, cover it with tests, and keep the integration thin enough to swap out. Recommend approving for content/admin surfaces only, with audit-grade logging explicitly out of scope.
For engineering: Drop-in Versionable trait, published config and single migration, per-model whitelist/blacklist, DIFF vs SNAPSHOT strategy, revert with and without saving, soft/force version deletion and restore, withoutVersion() for backfills, and diff output in array/HTML/side-by-side via an established diff library. Custom $versionModel lets us add actor and reason columns without forking. Known costs to plan for: an extra insert per versioned save, a versions table that must have a retention cap from day one, and the need for our own authorization layer around revert. Mitigations: pin, lock, integration tests per versioned model, and keep the trait behind our own service/repository boundary so a future replacement is a contained change.
Recommendation: Adopt for content and admin-editing models where rollback and change visibility are the product requirement. Decline for anything that must withstand an auditor, and revisit the build-vs-buy question if a second consumer (non-Laravel service, or cross-entity snapshots) appears — at that point a dedicated history service earns its cost.