saade/filament-autograph
Filament Autograph adds a signature pad field to Filament forms, letting users draw handwritten signatures with mouse or touch. Signatures are saved as images or base64 for contracts, waivers, and approvals, with simple configuration and validation.
The package is a single Filament form field — nothing else. Treat setup as four steps:
Install and pin.
composer require saade/filament-autograph
# then immediately tighten the constraint in composer.json, e.g. "saade/filament-autograph": "^4.3"
Pin the minor, not just the major. Fluent method names on this field have drifted between releases, and an unpinned composer update is the classic source of "my ->disk() call silently does nothing".
Publish config (and views only if you must restyle).
php artisan vendor:publish --tag=filament-autograph-config
php artisan vendor:publish --tag=filament-autograph-views # optional
The config holds the canvas defaults — height, pen colour, background, dot size. Set sensible defaults there once rather than repeating fluent calls on every field.
Add the column. Nullable, because the field must be clearable and because you'll want the form working before signatures exist.
$table->longText('signature')->nullable(); // base64 path
// or
$table->string('signature_path')->nullable(); // disk path (preferred)
Drop the field into a form you already have.
use Saade\FilamentAutograph\Forms\Components\SignaturePad;
SignaturePad::make('signature')
->label('Customer signature')
->canvasHeight(180)
->penColor('#111827')
->backgroundColor('#ffffff')
->required()
->columnSpanFull();
First use case: a proof-of-delivery or acknowledgement checkbox on an existing resource edit page. One nullable column, one field on one form, no changes to the surrounding schema. Run it on a real phone in the field, not on your desktop — that's the only test that matters at this stage.
Where to look first: the package's src/Forms/Components/SignaturePad.php (it's a small class — read the whole thing in five minutes and you'll know the real API surface), the published config, and the Blade view to see whether the canvas is initialised with Alpine on x-init or on Livewire hooks.
Wrap it, always. Never reference Saade\FilamentAutograph\... from a resource. Define one class:
namespace App\Filament\Forms\Components;
use Saade\FilamentAutograph\Forms\Components\SignaturePad as BaseSignaturePad;
class SignatureField extends BaseSignaturePad
{
protected function setUp(): void
{
parent::setUp();
$this->canvasHeight(180)
->penColor('#111827')
->backgroundColor('#ffffff')
->required();
}
}
One file to change when the upstream package goes dormant, breaks on a Filament major, or needs to be swapped for a FileUpload. Keep the diff against upstream clean.
Prefer disk-plus-path over base64-in-column. Base64 rides through Livewire component state, inflates the request body, and turns your table into a bloated blob store that's expensive to back up. If the version you pinned exposes ->disk(), ->directory() and ->visibility(), use them. If it doesn't, keep the field raw and normalise on save:
protected static function booted(): void
{
static::saving(function (Model $model) {
$raw = $model->signature; // data:image/png;base64,....
if (! is_string($raw) || ! str_starts_with($raw, 'data:image')) {
return;
}
$binary = base64_decode(Str::after($raw, ','), true);
abort_if($binary === false, 422, 'Invalid signature payload');
$path = 'signatures/'.Str::uuid().'.png';
Storage::disk('private')->put($path, $binary);
$model->signature_path = $path;
$model->signature_hash = hash('sha256', $binary);
$model->signature = null; // don't keep the blob
});
}
Treat the image as evidence, not as the record. Store a sibling row (or columns) with: signer id, signed_at, IP, user agent, the document/template version, and the SHA-256 of the image. A PNG on its own is not a defensible signature record.
Reuse the schema between create and edit, but lock it down on edit. Signatures are append-only; a signature captured once should not be silently redrawn. On edit forms, render a read-only view:
ImageEntry::make('signature_path')
->disk('private')
->visible(fn ($record) => $record !== null);
Multi-signature forms (e.g. customer + technician) are just two fields on two columns, or a morphMany signatures relation if you expect more than two and want per-signer timestamps.
Testing is straightforward because state is a string. Generate a 1×1 PNG fixture, fillForm(['signature' => $dataUrl]), submit, and assert the row and the file exist — plus a negative test that a blank canvas is rejected.
PDF output is a separate concern; render Storage::disk('private')->path($model->signature_path) into the PDF with dompdf/snappy. Coordinate placement onto a pre-existing PDF is not what this package does.
->required() may be satisfied by an empty data URL or an empty string, and the form will happily save a null signature with no exception. Validate server-side — in a saving hook or model observer — that the decoded binary is non-empty and decodable. Never trust client-side required state.base64_decode. Naively decoding data:image/png;base64,... yields a corrupt file that opens in nothing.post_max_size and upload_max_filesize cap your signature size, and a rejected oversized payload fails as a 413 or a silent field loss. Log and alert on it.->live() on a signature field round-trips a large string on every mouse-up.dark class (fixed in 4.3.2) — so the canvas looks right in dark mode even when the stored pixels are wrong.Section, a non-active Tabs tab, or a Wizard step and the canvas can initialise at width 0: signatures come out squashed, offset, or capture nothing. Test the field in its real container, and prefer an explicit pixel canvasHeight over percentage sizing.saving hook must handle null without throwing.How can I help you explore Laravel packages today?