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