Auto-Save Draft & Crash Recovery

Filament 4/5

Add auto-saving drafts with crash recovery to a Filament create/edit page — if you close the tab or crash, Filament offers to restore your unsaved work.

new-project-annotated-v4

Get the Source Code:

Only This Example

$9

One-time payment

Full source code for Auto-Save Draft & Crash Recovery
Downloadable ZIP file with the source code
Lifetime access to this example
GitHub Sign in with GitHub to buy

Sign in first, then complete your $9 checkout.

Best value — all 172 examples

FilamentExamples Membership

$99 /year
or
$199 lifetime
Access to code of all 172 examples
Future new examples and updates included
FilaCheck Pro package licence included
MCP server included
View membership plans

30-day money-back guarantee

How it works

The feature is made of five small pieces: a table to hold drafts, a RecoversContentDraft trait that does all the work, the two Post pages that opt in, a scoped render hook that injects the poller, and the Blade view that actually ticks. Nothing overrides Filament internals — it rides entirely on standard Livewire polling, events, and notification actions.

1. Where the draft lives — the content_drafts table and model

A draft is just the raw form state stored as JSON, owned by a user and identified by a stable key.

database/migrations/xxxx_create_content_drafts_table.php

Schema::create('content_drafts', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(User::class)->constrained()->cascadeOnDelete();
$table->string('key');
$table->json('payload');
$table->timestamps();
 
$table->unique(['user_id', 'key']);
});

app/Models/ContentDraft.php

#[Fillable(['user_id', 'key', 'payload'])]
class ContentDraft extends Model
{
protected function casts(): array
{
return [
'payload' => 'array',
];
}
}

The key points:

  • The unique(['user_id', 'key']) constraint is what makes auto-save idempotent — each user has at most one draft per page, so the trait can updateOrCreate() on every poll instead of piling up rows; the poller overwrites the same record forever
  • payload is a plain JSON column cast to array — the form's entire $this->data is dumped in verbatim, so recovery works for any field the form has (text, selects, repeaters) without a column per input
  • The key is a human-readable string like post-create or post-edit-5, not a foreign key to the post — a create page has no record yet, so keying by intent rather than by model is what lets the same table serve both create and edit

2. The auto-save engine — the RecoversContentDraft trait

All the behaviour lives in one trait a page can use. It snapshots $this->data on demand and knows how to find, restore, and clear the draft. Each page supplies its own key.

app/Filament/Concerns/RecoversContentDraft.php

abstract protected function contentDraftKey(): string;
 
public function saveDraft(): void
{
$data = $this->data ?? [];
 
if (blank($data['title'] ?? null) && blank($data['content'] ?? null)) {
return;
}
 
if ($this->matchesContentDraftReferenceState($data)) {
$this->clearContentDraft();
 
return;
}
 
ContentDraft::query()->updateOrCreate(
['user_id' => Auth::id(), 'key' => $this->contentDraftKey()],
['payload' => $data],
);
 
$this->dispatch('content-draft-saved');
}

The key points:

  • contentDraftKey() is abstract — the trait refuses to be used without a page telling it which draft it owns, which is exactly what keeps the create page's draft from colliding with an edit page's draft (see section 4)
  • The blank-check bails out before touching the database — an empty form isn't worth recovering, so polling an untouched page is free; only once there's a title or content does a row get written
  • updateOrCreate() keyed on user_id + key reuses the one unique row — this is the payoff of the unique index: the tenth poll updates the same draft the first poll created, so the table never grows while you type
  • saveDraft() dispatches content-draft-saved rather than sending a notification — a full notification every 10 seconds would be noise, so the trait just fires a browser event that the view turns into a subtle "Draft saved at …" line (section 5)

3. Not recovering what didn't change — the reference state

Auto-save is useful on create pages, but on an edit page you don't want to be prompted to "recover" a draft that's identical to the record already in the database. The trait compares the live form against a reference snapshot and skips (and clears) matching drafts.

The FULL tutorial is available after the purchase: in the Readme file of the official repository you would get invited to.