[TERMINAL · SKILLS]
> mounting /skills...
> indexing skill manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
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.

Development#typescript#tsconfig#type-checking#type-safety#javascript-migration
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 6 skills

Avg quality 95/100·All SAFE
>

typescript

v1.0.0

Sets up and repairs TypeScript projects: picks tsconfig.json settings for a Node.js service, an npm library or a bundled web app, runs type-checking in CI, and replaces `any` and unsafe casts with types the compiler can verify. Use when someone asks to "set up TypeScript", "fix this tsconfig", "fix a TypeScript error", "upgrade to TypeScript 6 or 7", "migrate JavaScript to TypeScript", "publish types for a package", "run .ts files in Node" or "make this type-safe". Covers the changed defaults in TypeScript 6.0 and 7.0, module and moduleResolution, strict mode, project references, declaration output, narrowing, satisfies and common compiler errors.

93/100 quality
7.30× impact
SAFE· stale score
View skill
>

redis

v1.1.0

Redis is an in-memory data store used as a cache, session store, message broker and counter service. Use when adding a cache layer, storing sessions, rate limiting an API, running pub/sub or Streams with consumer groups, taking a distributed lock, building a leaderboard, or configuring Redis for production (persistence, maxmemory, eviction).

93/100 quality
1.79× impact
SAFE· stale score
View skill
>

postgresql

v1.1.0

Assists with designing schemas, writing performant queries, managing indexes, and operating PostgreSQL databases. Use when working with JSONB, full-text search, window functions, CTEs, row-level security, replication, or performance tuning. Trigger words: postgresql, postgres, sql, database, jsonb, rls, window functions, cte.

100/100 quality
5.15× impact
SAFE· stale score
View skill
>

hono

v1.1.0

Hono is a small web framework built on Web Standards (Request/Response) that runs unchanged on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda and Vercel. Use it when the user asks to build a Hono API, add Zod validation, JWT auth, CORS or middleware, use the type-safe RPC client (hc), render JSX, stream responses, or deploy one codebase to several runtimes.

100/100 quality
6.20× impact
SAFE· stale score
View skill
>

zod

v1.1.0

Zod is a TypeScript-first schema library that validates data at runtime and infers static types from the same schema. Use when validating API request bodies, form input, environment variables or config files, parsing untrusted JSON, or migrating from Zod 3 to Zod 4. Covers z.object, safeParse, z.infer, coercion, discriminated unions, refinements, JSON Schema output and the v4 API changes.

93/100 quality
3.87× impact
SAFE· stale score
View skill
>

s3-storage

v1.1.0

Manages S3-compatible object storage (AWS S3, MinIO, Cloudflare R2, DigitalOcean Spaces, Backblaze B2, Wasabi, Supabase Storage). Use when the user wants to create buckets, upload/download files, set up lifecycle policies, configure CORS, manage presigned URLs, implement multipart uploads, set up replication, handle versioning, configure access policies, or build file management features on top of S3-compatible APIs. Trigger words: s3, minio, r2, object storage, bucket, presigned url, multipart upload, lifecycle policy, s3 cors, storage backend, file storage, blob storage, spaces, backblaze, wasabi.

93/100 quality
2.35× impact
SAFE· stale score
View skill
$

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

Related use cases