Engineering Deep Dives

Case Study: Building a Native E-Signature Module for SaaS Workflows

Case Study: Building a Native E-Signature Module for SaaS Workflows

Share

Time to read :

1 min read

Client Background

The Client: An established B2B SaaS platform providing workflow automation tools specifically designed for digital marketing agencies and consultancies.

The Context: The platform allows agencies to manage projects, manage customers, onboard clients, and track their agency performance.

The Problem: Despite handling the majority of their workflow on the platform, users faced a critical disconnect when finalizing contracts, forcing them to switch to external tools. The objective was to eliminate this friction by integrating a proprietary e-signature module, centralizing the entire agency workflow into a single interface.

Business Objective

The goal was to build a fully integrated, legally binding E-Signature module ("eSign") within the existing ecosystem to increase user retention and platform stickiness.

Key Technical Requirements:

  • Seamless Integration: A Drag-and-Drop editor enabling users to place signature fields, text inputs, and dates on any uploaded PDF.

  • Security: Secure storage of documents and audit trails (IP address, timestamps).

  • Asynchronous Workflow: Automated email triggers via SendGrid for signing requests and final document delivery.

  • Performance: High-fidelity PDF regeneration without rasterization (pixelation) issues.

Technical Solution

High-Level Architecture

We utilized a decoupled architecture. The frontend handles the visual placement of coordinates, while the backend handles the cryptographic manipulation of the PDF buffer.

Tech Stack:

  • Frontend: React.js, react-dnd (Drag and Drop), react-pdf (Viewer).

  • Backend: Node.js (Express), pdf-lib (PDF manipulation), AWS S3 (Storage).

  • Communication: SendGrid API (Transactional Emails).

Phase 1: The Document Editor (Frontend)

Instead of converting the PDF to images (which results in large files and non-selectable text), we rendered the PDF in the browser using pdf.js. We then overlaid a transparent HTML5 canvas layer using React-DnD.

When a user drops a "Signature Box" onto the document, we don't alter the PDF yet. We calculate the coordinates relative to the viewport and normalize them to the PDF's point system.

The Coordinate Challenge:

$$X_{pdf} = X_{screen} \times (\frac{W_{pdf}}{W_{screen}})$$

$$Y_{pdf} = Y_{screen} \times (\frac{H_{pdf}}{H_{screen}})$$

Where $W$ is width and $H$ is height.

Code Snippet: Tracking Drop Coordinates (React)

import { useDrop } from 'react-dnd';

const PDFPageOverlay = ({ pageDimensions, onDropItem }) => {
  const [, drop] = useDrop({
    accept: 'FIELD_TYPE',
    drop: (item, monitor) => {
      const delta = monitor.getClientOffset();
      const pdfOffset = monitor.getSourceClientOffset();
      
      // Calculate relative coordinates (0-100%) to handle responsiveness
      const relativeX = (pdfOffset.x / pageDimensions.width) * 100;
      const relativeY = (pdfOffset.y / pageDimensions.height) * 100;

      onDropItem({ x: relativeX, y: relativeY, type: item.type });
    },
  });

  return <div ref={drop} className="overlay-layer" />;
};

import { useDrop } from 'react-dnd';

const PDFPageOverlay = ({ pageDimensions, onDropItem }) => {
  const [, drop] = useDrop({
    accept: 'FIELD_TYPE',
    drop: (item, monitor) => {
      const delta = monitor.getClientOffset();
      const pdfOffset = monitor.getSourceClientOffset();
      
      // Calculate relative coordinates (0-100%) to handle responsiveness
      const relativeX = (pdfOffset.x / pageDimensions.width) * 100;
      const relativeY = (pdfOffset.y / pageDimensions.height) * 100;

      onDropItem({ x: relativeX, y: relativeY, type: item.type });
    },
  });

  return <div ref={drop} className="overlay-layer" />;
};

import { useDrop } from 'react-dnd';

const PDFPageOverlay = ({ pageDimensions, onDropItem }) => {
  const [, drop] = useDrop({
    accept: 'FIELD_TYPE',
    drop: (item, monitor) => {
      const delta = monitor.getClientOffset();
      const pdfOffset = monitor.getSourceClientOffset();
      
      // Calculate relative coordinates (0-100%) to handle responsiveness
      const relativeX = (pdfOffset.x / pageDimensions.width) * 100;
      const relativeY = (pdfOffset.y / pageDimensions.height) * 100;

      onDropItem({ x: relativeX, y: relativeY, type: item.type });
    },
  });

  return <div ref={drop} className="overlay-layer" />;
};

import { useDrop } from 'react-dnd';

const PDFPageOverlay = ({ pageDimensions, onDropItem }) => {
  const [, drop] = useDrop({
    accept: 'FIELD_TYPE',
    drop: (item, monitor) => {
      const delta = monitor.getClientOffset();
      const pdfOffset = monitor.getSourceClientOffset();
      
      // Calculate relative coordinates (0-100%) to handle responsiveness
      const relativeX = (pdfOffset.x / pageDimensions.width) * 100;
      const relativeY = (pdfOffset.y / pageDimensions.height) * 100;

      onDropItem({ x: relativeX, y: relativeY, type: item.type });
    },
  });

  return <div ref={drop} className="overlay-layer" />;
};

Phase 2: The Signing Flow & Backend Processing

Once the document is prepared, the backend creates a database entry for the "Envelope" containing the S3 key of the original PDF and the JSON array of field coordinates.

  • Trigger: An email is sent via SendGrid with a unique, signed URL token.

  • Action: The recipient opens the link, draws their signature on an HTML Canvas, or types it (converted to a font-based SVG).

  • Processing: Upon submission, the Node.js backend pulls the original PDF from S3.

Phase 3: PDF Reconstruction (The Core Logic)

PDF Reconstruction (The Core Logic)

This is the most critical part of engineering. We use pdf-lib to load the binary data of the PDF and inject the signature PNG/SVG at the calculated coordinates.

Why this approach?

  • Performance: Modifying the PDF structure is faster than image processing.

  • Quality: The original text remains vector-based (zoomable without blur).

Code Snippet: Backend PDF Injection (Node.js)

import { PDFDocument } from 'pdf-lib';
import fetch from 'node-fetch';

async function sealDocument(originalPdfUrl, signatureImageBuffer, coordinates) {
  // 1. Load the existing PDF
  const existingPdfBytes = await fetch(originalPdfUrl).then(res => res.arrayBuffer());
  const pdfDoc = await PDFDocument.load(existingPdfBytes);
  
  // 2. Embed the signature image
  const signatureImage = await pdfDoc.embedPng(signatureImageBuffer);
  const pages = pdfDoc.getPages();
  const currPage = pages[coordinates.pageNumber - 1]; // 0-index fix

  // 3. Draw the signature using the normalized coordinates
  // Note: PDF coordinates start at bottom-left, web starts top-left.
  // We must flip the Y axis: (PageHeight - Y - ImageHeight)
  const { width, height } = currPage.getSize();
  
  currPage.drawImage(signatureImage, {
    x: coordinates.x, 
    y: height - coordinates.y - 50, // Adjusting for coordinate flip
    width: 150,
    height: 50,
  });

  // 4. Save and return buffer
  const pdfBytes = await pdfDoc.save();
  return pdfBytes;
}

import { PDFDocument } from 'pdf-lib';
import fetch from 'node-fetch';

async function sealDocument(originalPdfUrl, signatureImageBuffer, coordinates) {
  // 1. Load the existing PDF
  const existingPdfBytes = await fetch(originalPdfUrl).then(res => res.arrayBuffer());
  const pdfDoc = await PDFDocument.load(existingPdfBytes);
  
  // 2. Embed the signature image
  const signatureImage = await pdfDoc.embedPng(signatureImageBuffer);
  const pages = pdfDoc.getPages();
  const currPage = pages[coordinates.pageNumber - 1]; // 0-index fix

  // 3. Draw the signature using the normalized coordinates
  // Note: PDF coordinates start at bottom-left, web starts top-left.
  // We must flip the Y axis: (PageHeight - Y - ImageHeight)
  const { width, height } = currPage.getSize();
  
  currPage.drawImage(signatureImage, {
    x: coordinates.x, 
    y: height - coordinates.y - 50, // Adjusting for coordinate flip
    width: 150,
    height: 50,
  });

  // 4. Save and return buffer
  const pdfBytes = await pdfDoc.save();
  return pdfBytes;
}

import { PDFDocument } from 'pdf-lib';
import fetch from 'node-fetch';

async function sealDocument(originalPdfUrl, signatureImageBuffer, coordinates) {
  // 1. Load the existing PDF
  const existingPdfBytes = await fetch(originalPdfUrl).then(res => res.arrayBuffer());
  const pdfDoc = await PDFDocument.load(existingPdfBytes);
  
  // 2. Embed the signature image
  const signatureImage = await pdfDoc.embedPng(signatureImageBuffer);
  const pages = pdfDoc.getPages();
  const currPage = pages[coordinates.pageNumber - 1]; // 0-index fix

  // 3. Draw the signature using the normalized coordinates
  // Note: PDF coordinates start at bottom-left, web starts top-left.
  // We must flip the Y axis: (PageHeight - Y - ImageHeight)
  const { width, height } = currPage.getSize();
  
  currPage.drawImage(signatureImage, {
    x: coordinates.x, 
    y: height - coordinates.y - 50, // Adjusting for coordinate flip
    width: 150,
    height: 50,
  });

  // 4. Save and return buffer
  const pdfBytes = await pdfDoc.save();
  return pdfBytes;
}

import { PDFDocument } from 'pdf-lib';
import fetch from 'node-fetch';

async function sealDocument(originalPdfUrl, signatureImageBuffer, coordinates) {
  // 1. Load the existing PDF
  const existingPdfBytes = await fetch(originalPdfUrl).then(res => res.arrayBuffer());
  const pdfDoc = await PDFDocument.load(existingPdfBytes);
  
  // 2. Embed the signature image
  const signatureImage = await pdfDoc.embedPng(signatureImageBuffer);
  const pages = pdfDoc.getPages();
  const currPage = pages[coordinates.pageNumber - 1]; // 0-index fix

  // 3. Draw the signature using the normalized coordinates
  // Note: PDF coordinates start at bottom-left, web starts top-left.
  // We must flip the Y axis: (PageHeight - Y - ImageHeight)
  const { width, height } = currPage.getSize();
  
  currPage.drawImage(signatureImage, {
    x: coordinates.x, 
    y: height - coordinates.y - 50, // Adjusting for coordinate flip
    width: 150,
    height: 50,
  });

  // 4. Save and return buffer
  const pdfBytes = await pdfDoc.save();
  return pdfBytes;
}

Phase 4: Delivery

The final Buffer is uploaded to S3 as a new file (e.g., contract_signed_v1.pdf). A parallel job triggers SendGrid to send an email with the PDF attached to both the sender and the signer.

Outcome

The introduction of the native E-Signature module transformed the client's platform from a project management tool into a complete operational ecosystem.

  • User Efficiency: Reduced document turnaround time by 40% by eliminating the need to download/upload to third-party tools.

  • Cost Savings: Saved the client approx. $2.00 per document in API fees that were previously paid to external providers like DocuSign.

  • Technical Scalability: The vector-based PDF generation reduced final file sizes by 60% compared to the initial image-based prototype, resulting in faster email delivery and lower storage costs.

Future Improvements

  • Implement Websockets to show real-time status updates (e.g., "Client is viewing the document").

  • Add Blockchain hashing for an immutable audit trail of the signed document.

Share

Deliverable Get in Touch

Mehak Mahajan

Customer Consultant

Contact with our team - we'll get back at lightning speed

We've experts in consulting, development, and marketing, Just tell us your goal, and we'll map a custom plan that fits your business needs.

phone call icon gif
Platform
Details
Budget
Contact
Company

What platform is your app development project for?