Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Drafts Laravel Package

oddvalue/laravel-drafts

A simple, drop-in drafts and revisions system for Laravel models. Add a trait to your model, update the database, and get published revisions, current revisions, auto drafts, and preview mode via middleware, with minimal changes to your existing code.

View on GitHub
Deep Wiki
Context7

Technical Evaluation

Architecture fit oddvalue/laravel-drafts is an Eloquent-native, single-table versioning system: every revision is a full row in the same table as the live record, correlated by a uuid column plus is_published / is_current flags, and a global scope transparently restricts reads to published rows. That is a good fit for editorial content models (pages, posts, product copy) where the unit of change is a whole document and where "preview the working copy" is a first-class requirement. It is a poor fit for high-write, high-cardinality domain tables, for anything whose primary key is referenced by foreign keys from other tables, and for models where partial-column updates must not be versioned. The design choice that matters most for architecture review is that it is not a side-table audit log — it is row multiplication in the primary table, which changes uniqueness, indexes, aggregates, and route-model binding semantics for every model it touches. The package is deliberately narrow in scope (flags, scopes, publish/copy relations, pruning), which is a strength: it composes with existing Eloquent conventions rather than introducing a parallel persistence layer.

Integration feasibility Integration cost is genuinely low per model: composer require, publish config, add the HasDrafts trait (optionally implement the Draftable contract), and run a migration using the $table->drafts() / $table->dropDrafts() schema helpers. Per-model column-name overrides via class constants (IS_CURRENT, etc.) and a $draftableRelations list keep the footprint configurable. The friction is not installation — it is retrofit. Existing tables need uuid backfilled to a stable value, flags set consistently, and unique constraints re-scoped (a slug unique index will break the moment a draft copies a published row). Optional features (auto drafts, scheduled publishing) are gated behind config and, per the README, no query references their columns while disabled — so they can be deferred, though adding is_auto and will_publish_at in the first migration avoids a second schema change later.

Technical risk

  • Global scope as a silent default. Any query on a draftable model that isn't explicitly withDrafts() / onlyDrafts() returns published rows only. Reports, counters, API resources, policy checks, and admin listings written before the trait existed will change behavior with no compiler warning. This is the single largest source of surprise defects.
  • Global mutable preview state. LaravelDrafts::previewMode() is process-wide. Under Octane, queue workers, or any long-lived process this can leak across requests/jobs unless scrupulously reset; the provided WithDraftsMiddleware is the safer pattern for route-scoped access.
  • Ecosystem signal is thin. 438 stars but zero GitHub dependents and a low opportunity score; effectively one maintainer, and the sibling oddvalue/filament-draft-recovery is referenced as not-yet-migrated to the new auto-draft API. Bus factor and long-term roadmap are the real risks, not code quality (tests, coverage, and CS workflows are present).
  • Version coupling. The Laravel matrix (v1 → 9/10, v2 → 11, >2.1|3.x → 12) means the package must be upgraded in lockstep with framework majors; a Laravel 13 gap would block a framework upgrade.
  • Publish-flow correctness. Publishing duplicates HasOne/HasMany children and syncs BelongsToMany/MorphToMany. Whether this is wrapped in a transaction, and how it behaves under concurrent publish attempts, is not documented.

Key questions

  1. Is the publish operation (flag flip + relation duplication + pruning) transactional, and is it idempotent under retry?
  2. What is the concurrency model for is_current when two editors work the same record — last-write-wins, or does a second saveAsDraft collide?
  3. Does current() always return exactly one row per uuid, including for records that were never drafted?
  4. Are child foreign keys (post_id on comments) expected to point at the published row only, and what breaks when they point at a revision?
  5. What are the pruning semantics for keep — oldest-first by created_at or id, and are published/current rows ever pruned?
  6. Is preview mode reset automatically at request/job boundaries, or is it entirely the caller's responsibility?
  7. What is the actual Packagist install base, and is there a stated policy for Laravel 13?
  8. When does Draftable become mandatory, and is there a supported upgrade path (the README references a Rector rule — confirm it's shipped)?

Integration Approach

Stack fit Fits any standard Laravel 9–12 application using Eloquent: Blade/Livewire/Inertia front ends, API resources, queue-driven content pipelines. It is framework-idiomatic (scopes, traits, middleware, artisan command, facade) and requires no new infrastructure — no Redis, no separate revisions store, no search dependency. Admin panel integration depends on the surrounding tooling; the sibling Filament auto-draft package is a signal of intent but is described as pre-release, so treat admin-panel UX as custom work in the initial plan.

Migration path

  1. Add the package, publish config, and set column names (keep defaults unless a table already has conflicting columns).
  2. Ship one migration per candidate model adding the draft columns and their indexes — at minimum a composite on (uuid, is_current), (uuid, is_published), and a rewritten unique index on any natural key (e.g. unique(['uuid', 'slug']) or a partial/filtered index where is_published = true).
  3. Backfill in the same or a follow-up migration: uuid from a stable identifier, is_published = true, published_at = created_at. Verify against real row counts before running on production-sized tables.
  4. Decide the public identifier strategy: if uuid becomes the route key for published records, existing URLs and inbound links must be preserved (keep the integer key as the public key, or add redirects).
  5. Add the trait and Draftable contract, then audit every existing query touching the model for the new default.

Compatibility Gate the package version to the Laravel major in use and pin it in composer.json with a caret constraint tested against the current framework version. Implement Draftable on new models immediately, since a future major is stated to require it. Enable auto_drafts and scheduled_drafts only after their columns exist; verify the LogicException behavior while disabled so tests don't accidentally depend on enabled-only paths. Note the publisher morph requires a configured auth guard — confirm it resolves the intended user model in non-web contexts (queues, artisan).

Sequencing Start with a single low-risk, high-value content model selected as a spike (a Page or a marketing Post) and take it end-to-end: schema, backfill, trait, admin "current" listing, publish action, public read path, and one publish-flow integration test. Only after that spike proves out index changes, route binding, and cache/search invalidation should the pattern be replicated model by model. Schedule scheduled-publishing enablement last, since it introduces a recurring artisan command per model that must be registered in the scheduler and observed in production.

Operational Impact

Maintenance Small surface area, MIT licensed, single maintainer, with changelog, tagged releases, and CI workflows present — healthy signals for a package of this size. The maintenance burden lands internally in three places: the publish orchestration code around the package, the index/uniqueness design on each draftable table, and the version-upgrade work tied to Laravel majors. Budget a recurring line item for each Laravel upgrade rather than treating this as install-and-forget. Vendor the config file into version control and treat column-name constants as an interface.

Support No commercial support or SLA; issue response depends on one maintainer. Escalation path must be internal: designate an owner (a senior backend engineer) who can read the source and patch a fork if needed. Keep the fork-on-short-notice option viable by pinning exact versions and keeping the diff surface small (prefer config over per-model constants where possible).

Scaling Storage and write amplification are the primary scaling concerns: each save inserts a row, and default retention is 10 revisions per record. For a content model with 50k records and an active editorial team, that is up to 500k rows — manageable, but the pruning job and the composite indexes must exist before volume arrives, or current() and published-read queries will degrade. Publishing duplicates child rows for HasOne/HasMany, so relation-heavy models multiply storage on every publish. Read paths should be checked for N+1 exposure introduced by per-row draft resolution. If the app runs Octane, verify preview mode is not retained across requests under load.

Failure modes

  • Drafts appearing in or vanishing from production listings after the trait is added (global scope), including in cached fragments and API responses.
  • Unique constraint violations on publish when a draft's slug duplicates another published record's slug.
  • Preview mode set on one request and honored on an unrelated one in Octane/queue contexts.
  • Partial publish: relations duplicated but the flag flip failing, or vice versa, leaving published and child data inconsistent.
  • Scheduled publishing silently never running because drafts:publish was not registered per model in the scheduler.
  • Pruning deleting more or less than intended if keep is changed without checking whether current/published rows are protected.
  • Orphaned child rows when records are hard-deleted outside the package's delete/restore hooks, or when soft-delete propagation is bypassed by raw queries.
  • Search index or CDN cache serving a stale version because invalidation was hooked to saved rather than publish.

Ramp-up Learning cost is low for anyone fluent in Eloquent scopes and traits — roughly half a day for a mid-level engineer to be productive on a single model, plus a day to internalize the row-per-revision mental model. The durable cost is team discipline: add a code-review checklist item asking "should this query see drafts?" for every new query on a draftable model, document the two access patterns (current() for admin, default for public) in the team's conventions, and add a test template that exercises both. Onboarding a new engineer to a codebase where some models are draftable and some are not is the realistic ramp-up risk, so make the draftable model list explicit in documentation rather than discoverable by reading traits.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log