Skip to content

Build custom signature capture with signature_pad.js and Rails

pattern

E-signature services like DocuSign are expensive ($25+/mo) for simple signature capture workflows

signaturerailsesignaturesignature-padpdf
22 views

Problem

Simple signature capture workflows (contracts, consent forms, delivery confirmations) do not justify $25-50/month for DocuSign or HelloSign. These services are designed for complex multi-party signing workflows with legal compliance features that most small applications do not need. For basic "draw your signature and attach it to a document" functionality, a custom solution is significantly cheaper and more flexible.

Solution

Step 1: Add signature_pad.js to your frontend

<!-- app/views/signatures/new.html.erb -->
<div class="signature-container">
  <canvas id="signature-pad" width="600" height="200"
    style="border: 1px solid #ccc; border-radius: 4px;">
  </canvas>

  <div class="signature-actions">
    <button type="button" id="clear-btn">Clear</button>
    <button type="button" id="save-btn">Save Signature</button>
  </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/signature_pad@4.2.0/dist/signature_pad.umd.min.js"></script>
<script>
  const canvas = document.getElementById('signature-pad');
  const signaturePad = new SignaturePad(canvas, {
    backgroundColor: 'rgb(255, 255, 255)',
    penColor: 'rgb(0, 0, 0)'
  });

  document.getElementById('clear-btn').addEventListener('click', () => {
    signaturePad.clear();
  });

  document.getElementById('save-btn').addEventListener('click', async () => {
    if (signaturePad.isEmpty()) {
      alert('Please provide a signature.');
      return;
    }
    const dataUrl = signaturePad.toDataURL('image/png');
    const response = await fetch('/signatures', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': document.querySelector('[name="csrf-token"]').content
      },
      body: JSON.stringify({ signature: dataUrl })
    });
    if (response.ok) window.location.href = '/signatures/success';
  });
</script>

Step 2: Handle the signature on the Rails backend

# app/controllers/signatures_controller.rb
class SignaturesController < ApplicationController
  def create
    data_url = params[:signature]
    image_data = Base64.decode64(data_url.split(',')[1])

    signature = Signature.new(
      user: current_user,
      document_id: params[:document_id],
      signed_at: Time.current
    )

    signature.image.attach(
      io: StringIO.new(image_data),
      filename: "signature_#{Time.current.to_i}.png",
      content_type: 'image/png'
    )

    if signature.save
      GenerateSignedPdfJob.perform_later(signature.id)
      render json: { status: 'ok' }
    else
      render json: { errors: signature.errors }, status: :unprocessable_entity
    end
  end
end

Step 3: Generate the signed PDF

# app/jobs/generate_signed_pdf_job.rb
class GenerateSignedPdfJob < ApplicationJob
  def perform(signature_id)
    signature = Signature.find(signature_id)
    pdf = Prawn::Document.new do |doc|
      doc.text "Agreement signed by #{signature.user.name}"
      doc.text "Date: #{signature.signed_at.strftime('%B %d, %Y')}"
      doc.move_down 20
      doc.image StringIO.new(signature.image.download), width: 200
    end

    signature.signed_pdf.attach(
      io: StringIO.new(pdf.render),
      filename: "signed_agreement_#{signature.id}.pdf",
      content_type: 'application/pdf'
    )
  end
end

Why It Works

signature_pad.js is a lightweight library (8KB) that captures smooth, natural-looking signatures on a canvas element. The signature is exported as a base64-encoded PNG, which Rails stores via Active Storage. Prawn handles PDF generation with the embedded signature image. The entire stack costs nothing beyond your existing hosting, compared to per-signature fees from e-signature providers.

Context

  • signature_pad.js works on both desktop (mouse) and mobile (touch) with pressure sensitivity
  • For legal compliance, store a timestamp, IP address, and user agent alongside the signature
  • Add toSVG() instead of toDataURL() for vector signatures that scale cleanly in PDFs
  • The Prawn gem can also overlay signatures onto existing PDF templates using pdf.image at specific coordinates
  • This pattern works with any backend (Node, Django, Phoenix) -- only the storage and PDF generation code changes
About this share
Contributormblode
Repositorymblode/shares
CreatedFeb 10, 2026
View on GitHub