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

Filament Draft Recovery Laravel Package

oddvalue/filament-draft-recovery

Auto-save and crash recovery for Filament v4/v5 create and edit pages. Form state is saved as a debounced draft (2s by default), so users can recover unsaved work after a browser crash, tab close, or lost connection. Swappable storage drivers included.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install the package (v1.x is the first stable release; pin it):

    composer require oddvalue/filament-draft-recovery:^1.0
    
  2. Publish the config (and migrations if you plan to use the database driver)

    php artisan vendor:publish --tag="filament-draft-recovery-config"
    php artisan vendor:publish --tag="filament-draft-recovery-migrations"
    php artisan migrate
    
  3. Register the plugin on the panel in your PanelProvider:

    use Oddvalue\FilamentDraftRecovery\FilamentDraftRecoveryPlugin;
    
    public function panel(Panel $panel): Panel
    {
        return $panel
            ->plugin(FilamentDraftRecoveryPlugin::make());
    }
    
  4. Where to look first

    • config/filament-draft-recovery.php — storage driver, autosave interval, enabled pages.
    • src/Drivers/ — the storage implementations (localStorage, database, laravel-drafts) and the contract you'd implement for a custom driver.
    • The trait/interface applied to your resource pages — since 1.0.0 this is also where draft clearing hooks live (see below), so read it before wiring up save logic.
    • The published migration — confirms the schema the database driver expects (user_id, resource/page key, payload, timestamps). Re-publish/diff it if you're upgrading from 0.5.x.
  5. First use case — a long-form resource (blog post, product, ticket) where losing 20 minutes of typing is unacceptable. Open the create page, type into a field, wait one autosave interval, close the tab, reopen the create page. You should be prompted to restore the draft. Then hit Save and confirm the draft is cleared from storage — that second half of the loop is the new 1.0.0 behaviour and worth validating on day one.

Implementation Patterns

Panel-wide vs. per-page opt-in. Registering the plugin at the panel level enables it globally; use the config's resource/page allow-list (or the page-level trait/interface the package exposes) to opt specific resources in or out. For mixed panels, I prefer enabling globally then excluding sensitive resources (e.g. anything with payment details or credentials) rather than the inverse — it's easier to audit.

Driver selection is an architectural decision, not a config tweak.

Driver Use when Trade-off
localStorage Default; internal tools, single-browser editors Drafts don't follow the user across devices; data sits unencrypted in the browser
database Multi-device editing, auditability, shared workstations Needs migration, authorization scoping, and a pruning strategy
laravel-drafts App already uses oddvalue/laravel-drafts Couples draft lifecycle to that package's models and retention rules

Draft lifecycle hooks (new in 1.0.0). Drafts can now be cleared via trait-named lifecycle hooks rather than you hand-rolling a deleteDraft() call in every afterSave/afterCreate. The practical pattern is: let the trait's hooks handle clearing on successful submit, and reserve your own overrides for the cases the trait doesn't cover (e.g. a custom "discard" action, or a bulk-import path that never fires page lifecycle events). Read the trait source and the tightened README before assuming a hook fires on your particular page type — edit pages and re-submission of the same record are the two flows I'd assert in tests.

Recovery flow integration. The package's value is the mount-time intercept: when a create/edit page boots and a draft exists, the user is asked to restore or discard. Keep your own mount()/mutateFormDataBeforeFill() logic compatible with that — if you override form data after hydration, you can silently clobber a restored draft. Test restore on edit pages where the record has changed since the draft was written; the package can't merge, so you need a policy (usually "restored draft wins" with a visible timestamp in the prompt).

Multi-step wizards and Repeaters. Drafts serialize the Livewire form state, so wizards restore the current step's data — but step navigation itself isn't restored. If a wizard's step is derived from a URL query param, the user lands on step 1 with the full draft behind it. Either persist the step in form state or accept the small UX wrinkle and document it.

Tenancy and authorization with the database driver. Scope drafts by tenant and user. If your panel is tenanted, verify the migration/key includes the tenant, otherwise a user moving between tenants can be offered a draft belonging to another context. Add a policy or query scope in a custom driver if the bundled one doesn't cover it.

Testing. Set the driver to an in-memory/fake implementation in phpunit.xml or a test case, and assert three things separately: (a) a draft is written after the interval, (b) the restore prompt appears on re-mount, (c) the draft row/key is gone after a successful save (this is the new hook behaviour — previously you'd have asserted your own cleanup code). Livewire tests can dispatch the input events and then hit the page's mount() again.

Custom driver extension point. Implement the package's driver contract and bind it in the config (or container, depending on how the config resolves it). This is the right hook for Redis, S3, or writing drafts into a drafts table you already own — don't fork the package for storage changes.

Gotchas and Tips

  • Upgrading from 0.5.x is a low-risk but not zero-risk bump. The 1.0.0 notes bundle dependency chores, README/artwork changes, and one behavioural change (trait-based draft clearing) — no deprecations or removed APIs are listed. Still: if you had wired your own draft deletion into afterSave/afterCreate, you may now be deleting twice or fighting the trait. Grep your resources for draft-cleanup code before bumping, and re-run your restore/save smoke test.
  • Only form state is recoverable. Uploaded files live in Livewire's temporary upload directory and are not part of the draft payload. Users who spend ten minutes attaching images and then crash will still lose them. Communicate this in the recovery prompt if your forms are upload-heavy.
  • Deferred wire:model fields can produce stale drafts. If fields are bound with wire:model (deferred) rather than wire:model.live, the server-side state the autosave request sees may lag the visible input. For critical fields, switch to live or live.debounce.500ms, then re-test the autosave loop.
  • Restoring is not saving. After a draft restore, the record is still unsaved. Users routinely assume "restored" means "saved." Pair the restore notification with an explicit "Save" call-to-action.
  • Config caching. Driver and interval changes require php artisan config:cache (or a cache clear) in deployed environments — a surprising number of "the driver won't switch" reports are stale config caches.
  • Database driver hygiene. Trait hooks now cover the happy path (draft cleared on successful submit), but they don't cover abandoned forms, validation-hard-failures, or drafts from users who never come back. Schedule a pruning command (older than N days) and consider Prunable modelling. Verify the "draft deleted after save" behaviour explicitly on edit pages — that's the flow most likely to leave an up-to-date record still prompting "restore this draft?".
  • Raw, pre-validation data in the database. The database driver stores whatever the user typed, including fields that would fail validation or contain PII. If that's a concern, use a custom driver that encrypts the payload (encrypted cast) and restrict the table via policy.
  • LocalStorage is per-origin, per-browser-profile. Incognito, a different browser, or a cleared site data wipes drafts. Set support expectations accordingly, and don't market this as "recovery" to end users on the localStorage driver without knowing its limits.
  • Now at 1.0.0 but still a small ecosystem. Reaching 1.0 signals a committed public API, which is a good reason to pin to ^1.0 rather than dev-main. Adoption remains low, so keep a smoke test for the restore and clear-on-save flows in your suite — a transitive Filament upgrade breaking a Livewire lifecycle hook is exactly the failure mode that would silently stop draft clearing.
  • Debugging checklist, in order: (1) is the plugin registered on this panel? (2) is the resource excluded by config? (3) does the autosave request appear in the network tab? (4) does the localStorage key / DB row exist? (5) does re-mounting the page trigger the prompt? (6) after saving, is the row/key gone? That sequence localises the failure to client, driver, recovery, or cleanup layer quickly.
  • Extension points to prefer — driver contract for storage, config for interval/allow-lists, the trait's lifecycle hooks for clear-on-save, and published views (if provided) for prompt styling. Avoid overriding page mount() methods; wrap or extend instead, since that's exactly where recovery hooks in.
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.
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log