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.
Install the package (v1.x is the first stable release; pin it):
composer require oddvalue/filament-draft-recovery:^1.0
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
Register the plugin on the panel in your PanelProvider:
use Oddvalue\FilamentDraftRecovery\FilamentDraftRecoveryPlugin;
public function panel(Panel $panel): Panel
{
return $panel
->plugin(FilamentDraftRecoveryPlugin::make());
}
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.user_id, resource/page key, payload, timestamps). Re-publish/diff it if you're upgrading from 0.5.x.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.
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.
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.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.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.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?".encrypted cast) and restrict the table via policy.^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.mount() methods; wrap or extend instead, since that's exactly where recovery hooks in.How can I help you explore Laravel packages today?