creagia/laravel-sign-pad
Laravel package for capturing pad signatures tied to Eloquent models. Adds signing flows, stores signatures and optional signed/certified PDF documents, with configurable disks, routes, and redirects. Includes install command, migrations, and publishable JS assets.
Install the package:
composer require creagia/laravel-sign-pad
php artisan sign-pad:install
php artisan vendor:publish --tag=sign-pad-assets
Prepare your model (e.g., Contract):
use Creagia\LaravelSignPad\Concerns\RequiresSignature;
use Creagia\LaravelSignPad\Contracts\CanBeSigned;
class Contract extends Model implements CanBeSigned
{
use RequiresSignature;
}
Add the signature form to your Blade view:
@if (!$contract->hasBeenSigned())
<form action="{{ $contract->getSignatureRoute() }}" method="POST">
@csrf
<x-creagia-signature-pad />
</form>
<script src="{{ asset('vendor/sign-pad/sign-pad.min.js') }}"></script>
@endif
Contract record.x-creagia-signature-pad.storage/app/signatures/{uuid}.png and attaches it to the Contract model via the signature relation.redirect_route_name in config/sign-pad.php).RequiresSignature to auto-load the signature relation and lifecycle methods (e.g., bootRequiresSignature for model events).CanBeSigned (mandatory) and optionally ShouldGenerateSignatureDocument for PDF generation.class Invoice extends Model implements CanBeSigned, ShouldGenerateSignatureDocument
{
use RequiresSignature;
public function getSignatureDocumentTemplate(): SignatureDocumentTemplate
{
return new SignatureDocumentTemplate(
signaturePositions: [new SignaturePosition(page: 1, x: 100, y: 200)]
);
}
}
resources/views/pdf/invoice.blade.php).PdfDocumentTemplate.signaturePositions: [
new SignaturePosition(page: 1, x: 50, y: 50),
new SignaturePosition(page: 2, x: 150, y: 100),
]
<x-creagia-signature-pad /> for styling/UX:
<x-creagia-signature-pad
border-color="#3b82f6"
pad-classes="border-2 border-dashed"
submit-name="✍️ Sign Contract"
:disabled-without-signature="true"
/>
<x-creagia-signature-pad id="pad-1" />
<x-creagia-signature-pad id="pad-2" />
$signaturePath = $contract->signature->getSignatureImageAbsolutePath();
$pdfPath = $contract->signature->getSignedDocumentAbsolutePath();
$contract->deleteSignature(); // Deletes image + DB record
storage/app/certificate.crt).config/sign-pad.php:
'certify_documents' => true,
'certificate_file' => storage_path('app/certificate.crt'),
'certificate_info' => [
'country' => 'US',
'state' => 'California',
// ...
],
config/sign-pad.php to use S3 or other disks:
'disk' => 's3',
'signature_path' => 'contracts/signatures',
'document_path' => 'contracts/documents',
use Creagia\LaravelSignPad\Jobs\GenerateSignatureDocument;
GenerateSignatureDocument::dispatch($contract)->onQueue('pdf-generation');
$contract->signature->generateDocument();
Mail::to($contract->user)->send(new ContractSigned($contract));
class ContractPolicy
{
public function sign(User $user, Contract $contract)
{
return $user->can('sign_contracts');
}
}
disabled-without-signature prop or add client-side validation:
document.querySelector('form').addEventListener('submit', (e) => {
if (!document.getElementById('signature-pad').isEmpty()) {
e.preventDefault();
alert('Please draw a signature first.');
}
});
certificate_file path in config/sign-pad.php.\TCPDF::setLogger(new \TCPDFLogger(\TCPDFLogger::LEVEL_DEBUG));
SignaturePosition with precise coordinates (test with page: 1, x: 100, y: 100 as a baseline).// In your controller
return view('pdf.contract', ['model' => $contract]);
config/sign-pad.php) has write permissions:
chmod -R 755 storage/app/signatures storage/app/documents
dd($contract->signature->toArray());
$this->assertFileExists($contract->signature->getSignatureImageAbsolutePath());
$contract->signature->generateDocument();
validateSignature method in your model:
public function validateSignature(array $data): bool
{
return strlen($data['signature']) > 100; // Custom logic
}
public function getSignatureDocumentTemplate(): SignatureDocumentTemplate
{
$positions = $this->signaturePositions()->get();
return new SignatureDocumentTemplate(
signaturePositions: $positions->map(fn ($p) => new SignaturePosition(
page: $p->page,
x: $p->x,
y: $p->y
))
);
}
// In your model's boot method
static::signed(function ($model) {
event(new ContractSignedEvent($model));
});
signature relation to store intermediate states:
$contract->signature->update(['status' => 'pending_review']);
redirect_route_name in config/sign-pad.php must match a namedHow can I help you explore Laravel packages today?