PHPackages                             oddvalue/filament-draft-recovery - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. oddvalue/filament-draft-recovery

ActiveLibrary

oddvalue/filament-draft-recovery
================================

Auto-save draft &amp; crash recovery for Filament create/edit pages, with swappable storage drivers (browser localStorage, database, or oddvalue/laravel-drafts)

v0.5.0(3w ago)44.0k1[1 PRs](https://github.com/oddvalue/filament-draft-recovery/pulls)MITPHPPHP ^8.2CI passing

Since Jul 16Pushed 1w agoCompare

[ Source](https://github.com/oddvalue/filament-draft-recovery)[ Packagist](https://packagist.org/packages/oddvalue/filament-draft-recovery)[ Docs](https://github.com/oddvalue/filament-draft-recovery)[ RSS](/packages/oddvalue-filament-draft-recovery/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (5)Dependencies (15)Versions (14)Used By (0)

   ![Filament Draft Recovery — auto-save & crash recovery for create and edit pages](art/draft-recovery-2560x1440-light.jpg)Filament Draft Recovery
=======================

[](#filament-draft-recovery)

[![Latest Version on Packagist](https://camo.githubusercontent.com/5f59fca49e3280b7cdfe84f09569014137f9d34ef579cd65ceb2cfeb68e41705/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6f646476616c75652f66696c616d656e742d64726166742d7265636f766572792e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/oddvalue/filament-draft-recovery)[![PHP Support](https://camo.githubusercontent.com/398f1952ca1d901ce94a7e41e15f0cd0d855f26b3feb00ae444ec0ec5aa7ba4b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f64796e616d69632f6a736f6e3f75726c3d68747470732533412532462532467261772e67697468756275736572636f6e74656e742e636f6d2532466f646476616c756525324666696c616d656e742d64726166742d7265636f766572792532466d61696e253246636f6d706f7365722e6a736f6e2671756572793d726571756972652e706870266c6162656c3d504850)](https://www.php.net/)[![Filament Support](https://camo.githubusercontent.com/11f1a3dd2fd3f0048d3ef1cd7753b7b513c1cd298aed6ce8f422a70b12724c3e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f64796e616d69632f6a736f6e3f75726c3d68747470732533412532462532467261772e67697468756275736572636f6e74656e742e636f6d2532466f646476616c756525324666696c616d656e742d64726166742d7265636f766572792532466d61696e253246636f6d706f7365722e6a736f6e2671756572793d726571756972652535422766696c616d656e7425324666696c616d656e7427253544266c6162656c3d46696c616d656e74)](https://filamentphp.com/)[![GitHub Tests Action Status](https://camo.githubusercontent.com/c0c4154ae661b6bc1db3d19b6699a7778f874738f6df4bd26a97d2cea2dbe2a0/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6f646476616c75652f66696c616d656e742d64726166742d7265636f766572792f74657374732e796d6c3f6c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/oddvalue/filament-draft-recovery/actions?query=workflow%3Atests+branch%3Amain)[![GitHub Code Style Action Status](https://camo.githubusercontent.com/2d67bd987a32f6b09dd6172235c96fb9b9aa6055cd7a99e0ae7b7bfa7eed7a9a/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6f646476616c75652f66696c616d656e742d64726166742d7265636f766572792f6669782d636f64652d7374796c652e796d6c3f6c6162656c3d636f64652532307374796c65267374796c653d666c61742d737175617265)](https://github.com/oddvalue/filament-draft-recovery/actions?query=workflow%3Afix-code-style+branch%3Amain)[![Total Downloads](https://camo.githubusercontent.com/bd97ebcd35da4be9382532645331a3e74341744226045984085b07651fc66ae7/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6f646476616c75652f66696c616d656e742d64726166742d7265636f766572792e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/oddvalue/filament-draft-recovery)[![Coverage](https://camo.githubusercontent.com/94b996b525e0a6d4446c1b82817d1f300f779cef768a3388905148dd429db12a/68747470733a2f2f696d672e736869656c64732e696f2f656e64706f696e743f75726c3d68747470733a2f2f676973742e67697468756275736572636f6e74656e742e636f6d2f6f646476616c75652f39646438653530386362323433333732386434326132353831393337373065622f7261772f66696c616d656e742d64726166742d7265636f766572792d636f626572747572612d636f7665726167652e6a736f6e)](https://github.com/oddvalue/filament-draft-recovery/actions)

Auto-save draft &amp; crash recovery for Filament v4 and v5 create/edit pages, with swappable storage drivers. 100% test coverage, enforced in CI.

While a user edits a create or edit form, the form state is auto-saved (debounced, 2s by default). If their browser crashes, the tab closes, or the session expires, returning to the page shows a persistent notification offering to **recover** or **discard** the draft. Drafts are cleared on a successful save and expire after 7 days.

Storage drivers
---------------

[](#storage-drivers)

DriverWhere drafts liveNotes`local-storage` (default)The user's browser localStorageZero server storage; drafts are plaintext on the user's machine (see [Security](#security--sensitive-data))`database`The `recoverable_drafts` tableDrafts follow the user across devices; payloads can be [encrypted at rest](#encrypting-database-drafts)`laravel-drafts`**On the model being edited**, via [oddvalue/laravel-drafts](https://github.com/oddvalue/laravel-drafts)Auto-saves become draft revisions of the record itselfCustom drivers can be registered with `DraftRecovery::extend()`.

Installation
------------

[](#installation)

```
composer require oddvalue/filament-draft-recovery

php artisan filament-draft-recovery:install
```

The install command publishes the config and (for the server-side drivers) the migrations. Skip running the migrations if you only use the `local-storage` driver.

Usage
-----

[](#usage)

Add the trait to a resource's create and/or edit page:

```
use Filament\Resources\Pages\CreateRecord;
use Oddvalue\FilamentDraftRecovery\Concerns\RecoversDrafts;

class CreatePost extends CreateRecord
{
    use RecoversDrafts;

    protected static string $resource = PostResource::class;
}
```

The trait injects the JavaScript via the page footer. Nothing else to wire up.

### Choosing a driver

[](#choosing-a-driver)

Set the default in `config/filament-draft-recovery.php` (or `FILAMENT_DRAFT_RECOVERY_STORE`):

```
'store' => 'database',
```

Per panel:

```
use Oddvalue\FilamentDraftRecovery\DraftRecoveryPlugin;

$panel->plugin(DraftRecoveryPlugin::make()->store('database'));
```

Per page:

```
class CreatePost extends CreateRecord
{
    use RecoversDrafts;

    protected ?string $draftStore = 'laravel-drafts';
}
```

### The laravel-drafts driver

[](#the-laravel-drafts-driver)

```
composer require oddvalue/laravel-drafts
```

Edit-page drafts are stored **directly on the model being edited** via laravel-drafts' first-class **auto draft** feature. Requirements: the model uses the `HasDrafts` trait, its table has the drafts columns (including `is_auto`), and auto drafts are enabled (`drafts.auto_drafts.enabled` in laravel-drafts' config).

- **Edit pages**: each auto-save calls `saveAsAutoDraft()` on the record. That is one quietly upserted working copy: never the current draft, never a new revision, read back via the record's `autoDraft()` relation. The record keeps `is_current`; intentional drafts (`$record->draft`) are untouched.
- **Create pages**: auto drafts only exist for existing records, so create-page drafts are **delegated to another store**: the `laravel-drafts.create_store` config value, falling back to your default store (or `database` when the default is `laravel-drafts` itself). Any driver works, including custom ones.
- **Clearing** (successful save / discard) calls `discardAutoDraft()`. Published rows, intentional drafts, and revision history are never touched.
- Saves are **best-effort**: payloads that violate column constraints (required fields not yet filled) are skipped and retried on the next auto-save.
- Only real table columns are persisted; form-only keys are dropped. Repeater/relation state is not covered by this driver. Use `database` if you need the full form payload.

### Custom drivers

[](#custom-drivers)

Implement `Oddvalue\FilamentDraftRecovery\Contracts\DraftStore` and register it in a service provider:

```
use Oddvalue\FilamentDraftRecovery\Contracts\DraftStore;
use Oddvalue\FilamentDraftRecovery\Data\DraftContext;
use Oddvalue\FilamentDraftRecovery\Data\RecoveredDraft;
use Oddvalue\FilamentDraftRecovery\Facades\DraftRecovery;

class RedisDraftStore implements DraftStore
{
    public function isClientSide(): bool
    {
        return false;
    }

    public function get(DraftContext $context): ?RecoveredDraft
    {
        $payload = Redis::get($context->key);

        return $payload ? new RecoveredDraft(data: json_decode($payload, true)) : null;
    }

    public function put(DraftContext $context, array $data): void
    {
        Redis::setex($context->key, 60 * 60 * 24 * 7, json_encode($data));
    }

    public function forget(DraftContext $context): void
    {
        Redis::del($context->key);
    }
}

// In a service provider:
DraftRecovery::extend('redis', fn () => new RedisDraftStore);
```

Then select it like any built-in driver (`'store' => 'redis'`, `DraftRecoveryPlugin::make()->store('redis')`, or `protected ?string $draftStore = 'redis';`).

Every method receives a `DraftContext` carrying the unique `key` (always sufficient for key/value stores) plus the page's `modelClass`, `operation` (`create`/`edit`), `record` (edit pages), and `userId`.

### Save debounce

[](#save-debounce)

Auto-saves fire after the user stops typing for `save_debounce_milliseconds` (default 2000). Change the default in the config:

```
'save_debounce_milliseconds' => 5000,
```

Or per page:

```
protected function draftRecoverySaveDebounceMilliseconds(): int
{
    return 5000;
}
```

### Security &amp; sensitive data

[](#security--sensitive-data)

Drafts are snapshots of raw form state. With the default `local-storage` driver they live **in plaintext in the browser's localStorage**, readable by anyone with access to the machine, the browser profile, or any script running on the page. No client-side scheme can change that, so treat `local-storage` as suitable for non-sensitive form data only, and point resources that handle sensitive data at a server-side driver:

```
protected ?string $draftStore = 'database';
```

Safeguards that apply out of the box:

- **Password inputs are never drafted.** Any `TextInput` with `->password()` in the form schema is excluded automatically, in every driver.
- **Common sensitive keys are excluded by default** via the `excluded_fields` config: `password`, `password_confirmation`, `current_password`, `token`, `api_token`, `secret`.
- **Other users' leftovers are pruned.** When a draft-enabled page loads, localStorage drafts belonging to a different user of the same browser are removed.
- **Logout purge.** An explicit logout (Laravel's `Logout` event) queues a short-lived cookie, and the next panel page render (normally the login redirect) clears all of the package's localStorage drafts (`purge_on_logout` config, enabled by default), so drafts never outlive a logout on a shared machine. Session expiry fires no `Logout` event, so drafts from an expired session stay recoverable. Server-side drafts are unaffected either way.

### Excluding fields

[](#excluding-fields)

Exclusions merge from two places and apply to every driver, client- and server-side. Globally, in the config:

```
'excluded_fields' => [
    // ...the defaults above,
    'billing.card_number',
    'members.*.ssn',
],
```

Per page, additive to the config:

```
protected function draftRecoveryExcludedFields(): array
{
    return ['internal_notes', 'items.*.access_code'];
}
```

Patterns use dot notation to reach nested state; `*` matches a single segment, such as repeater or builder item keys.

### File uploads

[](#file-uploads)

When a file is selected in a `FileUpload` field, Livewire immediately moves the bytes to its temporary upload disk; the form state only holds a marker pointing at that temporary file. Whether a draft can bring a pending (not yet saved) upload back depends on the driver:

- **Server-side drivers** (`database`, custom): pending upload markers are kept in the draft. At recovery time each marker is re-checked against Livewire's temporary upload disk: if the temporary file still exists, the upload is restored as a pending upload (and is saved normally when the form is submitted); if Livewire has already pruned it, that upload is silently dropped and the rest of the draft still recovers.
- **`local-storage`**: markers are always stripped. The browser cannot verify that the server-side temporary file still exists, and restoring a dead marker would break the upload field.
- **`laravel-drafts`**: draft data is intersected with the model's real table columns, and pending upload state never matches a column value, so pending uploads are not preserved by this driver.

Files **already attached to the record** (edit pages) are unaffected by all of this: they are stored paths rather than temporary markers, and always survive drafting.

**Limitations**

- The recovery window for pending uploads is bounded by Livewire's temporary file lifetime, not by `expiry_days`. On local disks Livewire deletes temporary uploads older than **24 hours** (triggered whenever a new upload happens); on S3 you configure expiry via a bucket lifecycle rule. A draft recovered later restores everything *except* its pending uploads.
- The draft only references Livewire's temporary file; it does not copy the bytes. In multi-server setups the temporary upload disk (`livewire.temporary_file_upload.disk`) must be shared (e.g. S3) for recovery to find the file.

### Encrypting database drafts

[](#encrypting-database-drafts)

The `database` driver stores payloads as plain JSON by default. To encrypt them at rest (Laravel's `encrypted:array` cast, using your app key):

```
'database' => [
    'model' => RecoverableDraft::class,
    'encrypt' => true,
],
```

The `payload` column must be a text-type column, because ciphertext does not fit a MySQL `json` column. The shipped migration uses `longText`; if you published an earlier version of the migration that used `json`, change the column type before enabling encryption.

Pages with their own lifecycle hooks
------------------------------------

[](#pages-with-their-own-lifecycle-hooks)

The trait clears drafts from `afterCreate()` / `afterSave()`. A page that defines its own version of either hook **silently overrides the trait's**. Call the clear method yourself:

```
protected function afterSave(): void
{
    $this->dispatchDraftRecoveryClear();

    // your own logic…
}
```

The same applies to `getFooter()`: if your page overrides it, include the view from `Oddvalue\FilamentDraftRecovery\Concerns\RecoversDrafts::getFooter()` in your footer.

> If Filament gains trait-named lifecycle hook support ([filamentphp/filament PR](https://github.com/oddvalue/filament/tree/feature/trait-named-lifecycle-hooks)), this caveat goes away.

How it works
------------

[](#how-it-works)

- The Alpine component (injected via the page footer) snapshots the Livewire form state (`$wire.data`) on input/change, debounced by `save_debounce_milliseconds` (default 2 seconds).
- With `local-storage`, drafts stay in the browser; with a server-side driver the payload is sent to the page via a Livewire call.
- On return, a differing draft triggers a persistent Filament notification with **Recover draft** / **Discard** actions. Recovery merges the draft over the current form state.
- On successful save the page dispatches `draft-recovery-clear`, removing the draft and stopping the auto-save timers.
- Drafts expire after `expiry_days` (default 7); expired localStorage entries, along with entries belonging to other users of the same browser, are pruned on page load, and logging out purges all of them (see [Security](#security--sensitive-data)).
- With a server-side driver, pending file uploads are drafted as Livewire temporary upload markers and validated against the temporary upload disk at recovery time (see [File uploads](#file-uploads)).

Testing
-------

[](#testing)

```
composer test
```

Credits
-------

[](#credits)

Inspired by the [auto-save draft &amp; crash recovery Filament example](https://filamentexamples.com/project/auto-save-draft-crash-recovery), reworked around client-side storage and swappable drivers. Started from the [Filament plugin skeleton](https://github.com/filamentphp/plugin-skeleton).

License
-------

[](#license)

MIT. See [LICENSE.md](LICENSE.md).

###  Health Score

47

—

FairBetter than 93% of packages

Maintenance96

Actively maintained with recent releases

Popularity30

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity43

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 92% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~4 days

Total

6

Last Release

25d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/8445ea4661367cd677e1c3caa05c7e4021f30ff5ca7978e9f436bc7fd7532bf5?d=identicon)[oddvalue](/maintainers/oddvalue)

---

Top Contributors

[![oddvalue](https://avatars.githubusercontent.com/u/10127404?v=4)](https://github.com/oddvalue "oddvalue (23 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (2 commits)")

---

Tags

laravelfilamentfilament-pluginfilamentphpdraftsoddvalueautosavefilament-draft-recovery

###  Code Quality

TestsPest

Static AnalysisPHPStan, Rector

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/oddvalue-filament-draft-recovery/health.svg)

```
[![Health](https://phpackages.com/badges/oddvalue-filament-draft-recovery/health.svg)](https://phpackages.com/packages/oddvalue-filament-draft-recovery)
```

###  Alternatives

[relaticle/custom-fields

User Defined Custom Fields for Laravel Filament

16472.9k](/packages/relaticle-custom-fields)[awcodes/filament-curator

A media picker plugin for FilamentPHP.

441414.6k29](/packages/awcodes-filament-curator)[rawilk/profile-filament-plugin

Profile &amp; MFA starter kit for filament.

3915.5k](/packages/rawilk-profile-filament-plugin)[backstage/mails

View logged mails and events in a beautiful Filament UI.

16429.7k](/packages/backstage-mails)[marcelweidum/filament-passkeys

Use passkeys in your filamentphp app

6769.9k2](/packages/marcelweidum-filament-passkeys)[stephenjude/filament-two-factor-authentication

Filament Two Factor Authentication: Google 2FA + Passkey Authentication

85264.8k10](/packages/stephenjude-filament-two-factor-authentication)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
