DEV Community

younes
younes

Posted on

How I Built a Free E-Signature Tool for the French Market with Next.js 15

Why Build Another E-Signature Tool?

DocuSign charges €10/month. HelloSign wants your credit card. And none of them feel right for a French freelancer who just needs to sign a bail or a devis.

So I built Signely — a free, privacy-first e-signature platform built specifically for the French market. No account required. No documents uploaded to a server. Everything runs in your browser.

The Tech Stack

  • Next.js 15 with App Router and React 19
  • TypeScript end-to-end
  • Tailwind CSS 3 for styling
  • pdf-lib for PDF generation
  • PDF.js (CDN) for PDF rendering
  • Canvas API for signature drawing
  • Vercel for hosting
  • localStorage for signature persistence

No database. No auth. No backend API. The entire app is essentially a static site with client-side logic.

Architecture: Privacy by Design

The key architectural decision was zero server-side document processing. Every competitor uploads your PDF to their servers. Signely doesn't.

User Flow:
    │
    ├── /creer → Draw or type signature
    │            └── Saved to localStorage (dataURL)
    │
    └── /signer → Upload PDF/image
                  ├── PDF.js renders to <canvas>
                  ├── User places signature via drag
                  ├── Canvas composites document + signature
                  └── pdf-lib generates final PDF
                       └── Download (never leaves browser)
Enter fullscreen mode Exit fullscreen mode

This means I don't need GDPR data processing agreements, server-side encryption, or document retention policies. The browser is the server.

Canvas-Based Signature Drawing

The signature pad uses the Canvas API with touch support for mobile. Here's the core approach:

export function SignatureCanvas({ onSignatureChange }: Props) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const [isDrawing, setIsDrawing] = useState(false);

  useEffect(() => {
    const ctx = canvasRef.current?.getContext("2d");
    if (!ctx) return;
    ctx.strokeStyle = "rgb(18, 18, 18)";
    ctx.lineWidth = 2.5;
    ctx.lineCap = "round";
    ctx.lineJoin = "round";
  }, []);

  const getCoordinates = (e: React.MouseEvent | React.TouchEvent) => {
    const canvas = canvasRef.current!;
    const rect = canvas.getBoundingClientRect();
    const scaleX = canvas.width / rect.width;
    const scaleY = canvas.height / rect.height;

    if ("touches" in e) {
      const touch = e.touches[0];
      return {
        x: (touch.clientX - rect.left) * scaleX,
        y: (touch.clientY - rect.top) * scaleY,
      };
    }
    return {
      x: (e.clientX - rect.left) * scaleX,
      y: (e.clientY - rect.top) * scaleY,
    };
  };

  // Mouse/touch handlers → beginPath, lineTo, stroke
  // On stop → canvas.toDataURL("image/png") → parent callback
}
Enter fullscreen mode Exit fullscreen mode

Key gotcha: The scaleX/scaleY calculation is essential. The canvas element's CSS size differs from its pixel dimensions. Without this correction, signatures draw offset on high-DPI screens.

I also added touch-none CSS to prevent scroll-while-drawing on mobile — a subtle but critical UX detail.

PDF Rendering + Signing with Canvas Compositing

The document signer renders PDFs using PDF.js loaded from CDN (to avoid Next.js bundling issues with canvas):

function loadPdfJs(): Promise<PDFJsLib> {
  if (window.pdfjsLib) return Promise.resolve(window.pdfjsLib);

  return new Promise((resolve, reject) => {
    const script = document.createElement("script");
    script.src = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js";
    script.onload = () => {
      window.pdfjsLib!.GlobalWorkerOptions.workerSrc =
        "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js";
      resolve(window.pdfjsLib!);
    };
    document.head.appendChild(script);
  });
}
Enter fullscreen mode Exit fullscreen mode

Once the PDF is rendered to a canvas, placing the signature is just canvas compositing:

const generateSignedDocument = async () => {
  const canvas = document.createElement("canvas");
  canvas.width = documentWidth;
  canvas.height = documentHeight;
  const ctx = canvas.getContext("2d")!;

  // Draw document, then signature on top
  ctx.drawImage(documentCanvas, 0, 0);
  ctx.drawImage(signatureImg, sigX, sigY, sigWidth, sigHeight);

  return canvas.toDataURL("image/png");
};
Enter fullscreen mode Exit fullscreen mode

For PDF export, I use pdf-lib to embed the signed image into a proper PDF:

import { PDFDocument } from "pdf-lib";

const pdfDoc = await PDFDocument.create();
const image = await pdfDoc.embedPng(signedImageBytes);
const { width, height } = image.scale(1);
const page = pdfDoc.addPage([width, height]);
page.drawImage(image, { x: 0, y: 0, width, height });
const pdfBytes = await pdfDoc.save();
Enter fullscreen mode Exit fullscreen mode

Users can download as PNG or PDF — both generated entirely client-side.

Building for the French Market

Language & UX

The entire UI is in French. Not "translated" French — native French. Button labels like "Créer ma Signature", "Télécharger en PDF", "Signer un autre document". Error messages, tooltips, FAQs — everything.

This sounds obvious, but most SaaS tools targeting France are English-first with a French translation bolted on. French users notice.

eIDAS Compliance

Signely implements simple electronic signatures (SES) under the EU eIDAS regulation (n°910/2014). This is the most common tier — legally valid for most everyday documents like contracts, leases, quotes, and NDAs.

The key requirements:

  • Intent to sign — the user explicitly places and confirms the signature
  • Document integrity — the signed document is a single composite (no separate layers)
  • Identification — the signature is visually linked to the signer

For advanced/qualified signatures (notarized acts, real estate), you'd need certificate-based signing. But for 95% of French business documents, SES is sufficient and legally binding.

RGPD (French GDPR)

Since no personal data leaves the browser, RGPD compliance is almost automatic. No cookies to consent to (we use Vercel Analytics, which is cookie-free). No user accounts. No document storage. The privacy page practically writes itself.

SEO Strategy for French Keywords

The French e-signature market searches for very specific terms:

  • "signature électronique en ligne" (~8K/mo)
  • "signer un document en ligne" (~5K/mo)
  • "signature électronique gratuite" (~3K/mo)

I built programmatic SEO pages for specific use cases — /cas-usages/bail, /cas-usages/contrat-travail, /cas-usages/devis — each targeting long-tail French keywords with structured data (FAQ schema, WebPage schema).

// Structured data for every page
export const metadata = buildPageMetadata({
  title: "Signature électronique en ligne | Signely",
  description: "Créez et signez vos documents en quelques secondes...",
  path: "/",
});

// FAQ schema for rich snippets
const jsonLd = [
  buildWebPageJsonLd({ path, title, description }),
  buildFaqPageJsonLd(faqEntries),
];
Enter fullscreen mode Exit fullscreen mode

Each use case page has its own FAQ, legal context, and step-by-step guide — all server-rendered for SEO.

What I'd Do Differently

  1. pdf-lib from the start — I initially tried manipulating PDFs with canvas only. pdf-lib is far better for maintaining PDF structure.
  2. Web Workers for PDF rendering — large PDFs can block the main thread. PDF.js supports workers but I should've prioritized this.
  3. IndexedDB over localStorage — signature dataURLs are large strings. IndexedDB handles binary data better.

Results

  • Fully functional e-signature tool with zero hosting costs (Vercel free tier)
  • Client-side architecture eliminates GDPR/data processing headaches
  • Targeting underserved French market with native-language UX
  • Sub-3-second signature workflow

Try It

🔗 Signely.fr — Free e-signature for the French market

Create a signature, upload a PDF, sign it, download it. Takes about 30 seconds. No signup required.


Have you built tools targeting a specific language/market? What challenges did you face with localization beyond just translating strings? I'd love to hear your approach in the comments.

Top comments (0)