eom-plus/nova-signature
A Laravel Nova field for capturing and storing handwritten signatures. Adds a signature pad to Nova forms, saves signature images or data with your models, and displays signatures in resource views for simple e-signature capture in admin panels.
Install the field like any other Nova package:
composer require eom-plus/nova-signature
Nova discovers the service provider automatically. You need a column to hold the drawn signature — the field typically persists the canvas output as a base64 data URL, so give it room:
Schema::table('contracts', function (Blueprint $table) {
$table->longText('signature')->nullable();
});
Attach it to a resource:
use EomPlus\NovaSignature\Signature;
public function fields(Request $request)
{
return [
ID::make()->sortable(),
Text::make('Title'),
Signature::make('Signature'),
];
}
Open a record, draw in the pad, and save. That is the first end-to-end use case: capturing a wet-style signature on a consent form, delivery note, or contract and persisting it to a single column.
Where to look first
src/Signature.php — the field class. This tells you the real API: whether it supports ->disk()/->path()/->storeAs(), canvas sizing modifiers, and how fill() and resolve() treat the stored value.resources/js/components/ (or dist/) — the Vue component. Confirm which signature-pad library and which Vue major version it was built against.README.md — usually shows the storage-as-file example; copy that rather than inventing your own.Raw base64 in a column (simplest). Drop Signature::make('Signature') on the resource and store the data URL directly. Rendering on the detail view is free if the field ships a display component; otherwise use ->displayUsing(fn ($value) => $value ? 'Signed' : 'Unsigned') and render the image yourself.
Store as a file. If the field exposes ->disk('public')->path('signatures') or ->storeAs(...), prefer it — the column then holds a path and your database stays small. Pair it with an accessor or ->thumbnail() for index views. If the field only emits base64, decode it in a model observer instead:
static::saved(function (Contract $contract) {
if (str_starts_with($contract->signature ?? '', 'data:image')) {
$path = 'signatures/'.$contract->id.'.png';
Storage::disk('public')->put(
$path,
base64_decode(explode(',', $contract->signature, 2)[1])
);
$contract->forceFill(['signature' => $path])->saveQuietly();
}
});
Gate the field on workflow state. Signature capture belongs to a specific step, not the whole record lifecycle:
Signature::make('Signature')
->canSee(fn () => $this->status === 'awaiting_signature')
->rules('required'),
Multiple signers. Two fields on the same resource (customer_signature, witness_signature) is cleaner than a polymorphic signatures table for straightforward forms. If signatures are versioned or audited, keep them in a separate signatures table and use the field only on the capture screen.
Pair with an Action. A common pattern is a "Sign" action whose modal shows the field, so signatures are appended without a full resource edit. Verify the canvas mounts — hidden-at-mount modals are the usual failure point (see Gotchas).
Customize the component. Override the Vue component via Nova::script() or by extending the field and setting $component, so you can add a "clear" button, change pen color, or embed a timestamp overlay.
Column size. A 600×200 PNG signature is routinely 10–50 KB of base64. string(255) will silently truncate. Use longText/mediumText.
Empty pads still submit a value. Signature Pad produces a blank (often fully transparent) data URL even when nothing was drawn. ->rules('required') passes because the string is non-empty. Add a custom rule that rejects the known-empty payload or checks decoded byte length:
Rule::notIn(['data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...']) // blank canvas
More robustly, validate on the server that the trimmed payload exceeds a small threshold.
Retina / canvas scaling. If you set CSS width/height without setting the canvas attributes, strokes land offset from the cursor on high-DPI screens. Set explicit pixel dimensions via the field's sizing modifiers and let CSS scale.
Modal and tab mounting. Inside Nova action modals or collapsed tabs the canvas can mount at zero width, producing an empty or squashed signature. Force a resize() on show, or render the field on a dedicated step.
Nova version risk. The last release is January 2024, stars and dependents are negligible, and Nova's asset pipeline changed across 4.x and 5.x. Test on your exact Nova/Vue version; if the component fails to register, you will usually see an unregistered-component warning in the browser console rather than a server error.
Hydration semantics differ by storage mode. With base64 storage the field hydrates the full data URL; with file storage it hydrates a path. Watch out for a resolveUsing that assumes one format — mixing both during a migration will produce broken <img> tags.
Payload bloat. Base64 signatures are heavy in JSON index responses. Always ->hideFromIndex() (or use a thumbnail) and avoid loading the field in list endpoints.
Debugging the saved value. Inspect the Nova update request in the Network tab to see exactly what the hidden input contains. Then confirm the model actually persisted it ($model->getRawOriginal('signature')); a mismatch usually means fill() is dropping the key or a saving observer is overwriting it.
Config and cleanup. If the package ships a config tag, publish it (php artisan vendor:publish --tag=nova-signature-config) to change defaults like pen color or canvas size. When storing to disk, add cleanup to the model's deleting event so orphaned signature files don't accumulate.
Extension points. Extend the field class, override the $component property, and register a replacement component through a small Nova asset package — this is the cleanest way to add features without forking. Keep the fork's Vue build script (npm run dev/prod) working locally so you can rebuild dist/ after changes.
How can I help you explore Laravel packages today?