Terminal.skills
Use Cases/Build an E-Signature System

Build an E-Signature System

Build a document signing system with PDF field placement, signing workflows, audit trails, multi-party signing order, reminders, and legal compliance — replacing DocuSign for internal workflows.

#redis#caching#database#pub-sub#queues
Works with:claude-codeopenai-codexgemini-clicursor
$

The Problem

Oscar leads ops at a 40-person company. They send 200+ contracts monthly using DocuSign at $25/user/month ($6K/year). Contracts follow a rigid flow: sales rep fills details → legal reviews → client signs → countersign. DocuSign handles signing but doesn't integrate with their CRM, can't enforce their approval chain, and the API costs extra. They need a signing system embedded in their app with custom workflows, audit trails, and multi-party signing order.

Step 1: Build the Signing Engine

typescript
// src/signing/engine.ts — Document signing with workflows and audit trails
import { randomBytes, createHash } from "node:crypto";
import { pool } from "../db";
import { Redis } from "ioredis";

const redis = new Redis(process.env.REDIS_URL!);

type SigningStatus = "draft" | "pending" | "in_progress" | "completed" | "declined" | "expired";

interface SigningRequest {
  id: string;
  documentId: string;
  documentUrl: string;
  title: string;
  status: SigningStatus;
  signers: Signer[];
  fields: SigningField[];
  createdBy: string;
  expiresAt: string | null;
  completedAt: string | null;
  auditTrail: AuditEvent[];
}

interface Signer {
  id: string;
  name: string;
  email: string;
  role: string;
  order: number;              // signing order (1 = first)
  status: "waiting" | "notified" | "viewed" | "signed" | "declined";
  signedAt: string | null;
  signatureData: string | null;
  ipAddress: string | null;
}

interface SigningField {
  id: string;
  signerId: string;
  type: "signature" | "initials" | "date" | "text" | "checkbox";
  page: number;
  x: number;
  y: number;
  width: number;
  height: number;
  required: boolean;
  value: string | null;
}

interface AuditEvent {
  action: string;
  actor: string;
  actorEmail: string;
  ipAddress: string;
  userAgent: string;
  timestamp: string;
  details?: string;
}

// Create signing request
export async function createSigningRequest(
  documentUrl: string,
  title: string,
  signers: Array<{ name: string; email: string; role: string; order: number }>,
  fields: Array<Omit<SigningField, "id" | "value">>,
  createdBy: string,
  expiresInDays: number = 30
): Promise<SigningRequest> {
  const id = `sign-${Date.now()}-${randomBytes(4).toString("hex")}`;
  const expiresAt = new Date(Date.now() + expiresInDays * 86400000).toISOString();

  const signerRecords: Signer[] = signers.map((s, i) => ({
    id: `signer-${i}-${randomBytes(3).toString("hex")}`,
    ...s,
    status: s.order === 1 ? "notified" : "waiting",
    signedAt: null, signatureData: null, ipAddress: null,
  }));

  const fieldRecords: SigningField[] = fields.map((f, i) => ({
    ...f, id: `field-${i}`, value: null,
  }));

  await pool.query(
    `INSERT INTO signing_requests (id, document_url, title, status, signers, fields, created_by, expires_at, audit_trail, created_at)
     VALUES ($1, $2, $3, 'pending', $4, $5, $6, $7, $8, NOW())`,
    [id, documentUrl, title, JSON.stringify(signerRecords), JSON.stringify(fieldRecords),
     createdBy, expiresAt, JSON.stringify([{
       action: "created", actor: createdBy, actorEmail: "", ipAddress: "", userAgent: "",
       timestamp: new Date().toISOString(),
     }])]
  );

  // Send notification to first signer
  const firstSigner = signerRecords.find((s) => s.order === 1);
  if (firstSigner) {
    await sendSigningNotification(id, firstSigner);
  }

  return {
    id, documentId: id, documentUrl, title, status: "pending",
    signers: signerRecords, fields: fieldRecords, createdBy,
    expiresAt, completedAt: null, auditTrail: [],
  };
}

// Sign document
export async function signDocument(
  requestId: string,
  signerId: string,
  fieldValues: Record<string, string>,
  signatureData: string,
  metadata: { ip: string; userAgent: string }
): Promise<{ success: boolean; allSigned: boolean; nextSigner: Signer | null }> {
  const { rows: [request] } = await pool.query("SELECT * FROM signing_requests WHERE id = $1", [requestId]);
  if (!request) throw new Error("Signing request not found");
  if (request.status === "completed" || request.status === "expired") throw new Error("Request is closed");

  // Check expiry
  if (request.expires_at && new Date(request.expires_at) < new Date()) {
    await pool.query("UPDATE signing_requests SET status = 'expired' WHERE id = $1", [requestId]);
    throw new Error("Signing request has expired");
  }

  const signers: Signer[] = JSON.parse(request.signers);
  const fields: SigningField[] = JSON.parse(request.fields);
  const auditTrail: AuditEvent[] = JSON.parse(request.audit_trail);

  const signer = signers.find((s) => s.id === signerId);
  if (!signer) throw new Error("Signer not found");
  if (signer.status === "signed") throw new Error("Already signed");

  // Verify it's this signer's turn
  const previousSigners = signers.filter((s) => s.order < signer.order);
  const allPreviousSigned = previousSigners.every((s) => s.status === "signed");
  if (!allPreviousSigned) throw new Error("Waiting for previous signers");

  // Validate required fields
  const signerFields = fields.filter((f) => f.signerId === signerId);
  for (const field of signerFields) {
    if (field.required && !fieldValues[field.id]) {
      throw new Error(`Field "${field.type}" is required`);
    }
    field.value = fieldValues[field.id] || null;
  }

  // Update signer
  signer.status = "signed";
  signer.signedAt = new Date().toISOString();
  signer.signatureData = signatureData;
  signer.ipAddress = metadata.ip;

  // Audit
  auditTrail.push({
    action: "signed", actor: signer.name, actorEmail: signer.email,
    ipAddress: metadata.ip, userAgent: metadata.userAgent,
    timestamp: new Date().toISOString(),
    details: `Signed as ${signer.role}`,
  });

  // Check if all signed
  const allSigned = signers.every((s) => s.status === "signed");
  const newStatus = allSigned ? "completed" : "in_progress";

  // Notify next signer
  let nextSigner: Signer | null = null;
  if (!allSigned) {
    nextSigner = signers.find((s) => s.status === "waiting" || s.status === "notified") || null;
    if (nextSigner) {
      nextSigner.status = "notified";
      await sendSigningNotification(requestId, nextSigner);
    }
  }

  await pool.query(
    `UPDATE signing_requests SET
       status = $2, signers = $3, fields = $4, audit_trail = $5,
       completed_at = $6
     WHERE id = $1`,
    [requestId, newStatus, JSON.stringify(signers), JSON.stringify(fields),
     JSON.stringify(auditTrail), allSigned ? new Date().toISOString() : null]
  );

  if (allSigned) {
    await generateSignedPDF(requestId);
  }

  return { success: true, allSigned, nextSigner };
}

// Decline to sign
export async function declineToSign(
  requestId: string, signerId: string, reason: string,
  metadata: { ip: string; userAgent: string }
): Promise<void> {
  const { rows: [request] } = await pool.query("SELECT * FROM signing_requests WHERE id = $1", [requestId]);
  const signers: Signer[] = JSON.parse(request.signers);
  const auditTrail: AuditEvent[] = JSON.parse(request.audit_trail);

  const signer = signers.find((s) => s.id === signerId)!;
  signer.status = "declined";

  auditTrail.push({
    action: "declined", actor: signer.name, actorEmail: signer.email,
    ipAddress: metadata.ip, userAgent: metadata.userAgent,
    timestamp: new Date().toISOString(), details: reason,
  });

  await pool.query(
    "UPDATE signing_requests SET status = 'declined', signers = $2, audit_trail = $3 WHERE id = $1",
    [requestId, JSON.stringify(signers), JSON.stringify(auditTrail)]
  );

  // Notify creator
  await redis.rpush("email:queue", JSON.stringify({
    type: "signing_declined", to: request.created_by,
    signerName: signer.name, reason, requestId,
  }));
}

async function sendSigningNotification(requestId: string, signer: Signer): Promise<void> {
  const token = randomBytes(32).toString("urlsafe-base64");
  await redis.setex(`sign:token:${token}`, 86400 * 30, JSON.stringify({ requestId, signerId: signer.id }));

  await redis.rpush("email:queue", JSON.stringify({
    type: "signing_request", to: signer.email,
    signerName: signer.name, signingUrl: `${process.env.APP_URL}/sign/${token}`,
  }));
}

async function generateSignedPDF(requestId: string): Promise<void> {
  // Flatten signatures onto PDF and store final document
}

Results

  • DocuSign cost: $6K/year → $0 — self-hosted signing with the same audit trail, multi-party workflow, and legal compliance
  • Signing integrated into app workflow — contracts created from CRM data, signed, and status synced back automatically; no context switching
  • Average signing time: 3 days → 4 hours — embedded signing link in email, one click to review and sign; mobile-friendly
  • Audit trail exceeds legal requirements — every action logged with timestamp, IP, user agent; tamper-evident chain of events
  • Sequential signing enforced — legal reviews before client sees the document; client signs before countersign; no out-of-order signing