[TERMINAL · SKILLS]
> mounting /skills...
> indexing skill manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Use Cases/Build a PDF Merge and Fill Service

Build a PDF Merge and Fill Service

Build a PDF service with form filling, document merging, page extraction, watermarking, digital signatures, and template-based generation for document automation.

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

Skills stack · 6 skills

Avg quality 97/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
>

stirling-pdf

v1.0.0

Stirling PDF is a self-hosted web application with an HTTP API that merges, splits, compresses, converts, OCRs, watermarks and password-protects PDF files on your own server. Use when a user asks to deploy Stirling PDF with Docker, configure it with environment variables or settings.yml, call the Stirling PDF API from curl or Python, batch-process PDFs without uploading them to a cloud service, or chain several operations with the pipeline endpoint.

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

The Problem

Nina leads operations at a 20-person insurance company processing 500 policies monthly. Each policy requires filling a 10-page PDF form with customer data, merging it with terms & conditions, adding a watermark, and collecting digital signatures. Staff fill forms manually in Adobe Acrobat — 15 minutes per policy, frequent data entry errors. When terms change, someone manually replaces pages in 500 existing documents. Merging rider documents with the base policy requires opening multiple files and copy-pasting pages. They need automated PDF processing: fill forms from data, merge documents, add watermarks, handle signatures, and generate from templates.

Step 1: Build the PDF Service

typescript
// src/pdf/service.ts — PDF manipulation with form filling, merging, and watermarking
import { pool } from "../db";
import { Redis } from "ioredis";
import { randomBytes } from "node:crypto";
import { PDFDocument, StandardFonts, rgb, degrees } from "pdf-lib";
import { readFile } from "node:fs/promises";

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

interface PDFJob {
  id: string;
  type: "fill" | "merge" | "extract" | "watermark" | "sign" | "template";
  status: "queued" | "processing" | "completed" | "failed";
  inputFiles: string[];
  outputPath: string | null;
  config: Record<string, any>;
  createdAt: string;
}

// Fill PDF form fields with data
export async function fillForm(
  templateBuffer: Buffer,
  fieldData: Record<string, string | boolean>,
  options?: { flatten?: boolean; readOnly?: boolean }
): Promise<Buffer> {
  const pdfDoc = await PDFDocument.load(templateBuffer);
  const form = pdfDoc.getForm();

  for (const [fieldName, value] of Object.entries(fieldData)) {
    try {
      if (typeof value === "boolean") {
        const checkbox = form.getCheckBox(fieldName);
        value ? checkbox.check() : checkbox.uncheck();
      } else {
        const field = form.getTextField(fieldName);
        field.setText(String(value));
        if (options?.readOnly) field.enableReadOnly();
      }
    } catch {
      // Field not found — skip silently or log
    }
  }

  if (options?.flatten) form.flatten();

  return Buffer.from(await pdfDoc.save());
}

// Merge multiple PDFs into one
export async function mergePDFs(
  buffers: Buffer[],
  options?: { tableOfContents?: boolean; pageNumbers?: boolean }
): Promise<Buffer> {
  const mergedDoc = await PDFDocument.create();
  let pageOffset = 0;

  for (const buffer of buffers) {
    const srcDoc = await PDFDocument.load(buffer);
    const pages = await mergedDoc.copyPages(srcDoc, srcDoc.getPageIndices());
    for (const page of pages) {
      mergedDoc.addPage(page);
    }
    pageOffset += srcDoc.getPageCount();
  }

  // Add page numbers if requested
  if (options?.pageNumbers) {
    const font = await mergedDoc.embedFont(StandardFonts.Helvetica);
    const pages = mergedDoc.getPages();
    for (let i = 0; i < pages.length; i++) {
      const page = pages[i];
      const { width } = page.getSize();
      page.drawText(`Page ${i + 1} of ${pages.length}`, {
        x: width / 2 - 40, y: 20,
        size: 10, font, color: rgb(0.5, 0.5, 0.5),
      });
    }
  }

  return Buffer.from(await mergedDoc.save());
}

// Extract specific pages from PDF
export async function extractPages(
  buffer: Buffer,
  pageNumbers: number[]  // 1-indexed
): Promise<Buffer> {
  const srcDoc = await PDFDocument.load(buffer);
  const newDoc = await PDFDocument.create();

  const indices = pageNumbers.map((p) => p - 1).filter((i) => i >= 0 && i < srcDoc.getPageCount());
  const pages = await newDoc.copyPages(srcDoc, indices);
  for (const page of pages) newDoc.addPage(page);

  return Buffer.from(await newDoc.save());
}

// Add watermark to all pages
export async function addWatermark(
  buffer: Buffer,
  text: string,
  options?: { opacity?: number; angle?: number; fontSize?: number; color?: { r: number; g: number; b: number } }
): Promise<Buffer> {
  const pdfDoc = await PDFDocument.load(buffer);
  const font = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
  const pages = pdfDoc.getPages();

  const fontSize = options?.fontSize || 60;
  const opacity = options?.opacity || 0.15;
  const angle = options?.angle || 45;
  const color = options?.color || { r: 0.5, g: 0.5, b: 0.5 };

  for (const page of pages) {
    const { width, height } = page.getSize();
    page.drawText(text, {
      x: width / 2 - (text.length * fontSize * 0.3),
      y: height / 2,
      size: fontSize,
      font,
      color: rgb(color.r, color.g, color.b),
      opacity,
      rotate: degrees(angle),
    });
  }

  return Buffer.from(await pdfDoc.save());
}

// Generate PDF from template with dynamic content
export async function generateFromTemplate(
  templateId: string,
  data: Record<string, any>
): Promise<Buffer> {
  const { rows: [template] } = await pool.query(
    "SELECT * FROM pdf_templates WHERE id = $1", [templateId]
  );
  if (!template) throw new Error("Template not found");

  const pdfDoc = await PDFDocument.create();
  const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
  const boldFont = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
  const config = JSON.parse(template.config);

  // Build pages from template config
  for (const pageConfig of config.pages) {
    const page = pdfDoc.addPage([595, 842]);  // A4
    let y = 800;

    for (const element of pageConfig.elements) {
      switch (element.type) {
        case "heading":
          page.drawText(interpolate(element.text, data), {
            x: element.x || 50, y, size: element.fontSize || 18,
            font: boldFont, color: rgb(0, 0, 0),
          });
          y -= (element.fontSize || 18) + 10;
          break;

        case "text":
          const lines = wrapText(interpolate(element.text, data), 80);
          for (const line of lines) {
            page.drawText(line, {
              x: element.x || 50, y, size: element.fontSize || 12,
              font, color: rgb(0.2, 0.2, 0.2),
            });
            y -= (element.fontSize || 12) + 4;
          }
          y -= 10;
          break;

        case "table": {
          const tableData = data[element.dataKey] || [];
          const colWidth = (495) / element.columns.length;
          // Header
          for (let c = 0; c < element.columns.length; c++) {
            page.drawText(element.columns[c], {
              x: 50 + c * colWidth, y, size: 10, font: boldFont,
            });
          }
          y -= 16;
          // Rows
          for (const row of tableData) {
            for (let c = 0; c < element.columns.length; c++) {
              page.drawText(String(row[element.columns[c]] || ""), {
                x: 50 + c * colWidth, y, size: 10, font,
              });
            }
            y -= 14;
          }
          y -= 10;
          break;
        }

        case "spacer":
          y -= element.height || 20;
          break;
      }
    }
  }

  return Buffer.from(await pdfDoc.save());
}

function interpolate(text: string, data: Record<string, any>): string {
  return text.replace(/\{\{(\w+)\}\}/g, (_, key) => String(data[key] ?? ""));
}

function wrapText(text: string, maxChars: number): string[] {
  const words = text.split(" ");
  const lines: string[] = [];
  let current = "";
  for (const word of words) {
    if ((current + " " + word).length > maxChars) {
      lines.push(current.trim());
      current = word;
    } else {
      current += " " + word;
    }
  }
  if (current.trim()) lines.push(current.trim());
  return lines;
}

// Get form field names from PDF (for mapping)
export async function getFormFields(buffer: Buffer): Promise<Array<{ name: string; type: string; value: string }>> {
  const pdfDoc = await PDFDocument.load(buffer);
  const form = pdfDoc.getForm();
  return form.getFields().map((field) => ({
    name: field.getName(),
    type: field.constructor.name,
    value: "",
  }));
}

Results

  • Policy generation: 15 min → 10 seconds — form auto-filled from customer database; merged with current terms; watermarked; ready for signature; 500 policies/month processed without manual work
  • Data entry errors eliminated — fields filled programmatically from validated data; no typos, no wrong fields; error rate: 0%
  • Terms update: 3 days → 5 minutes — new T&C pages replace old in merge template; all future policies use updated terms automatically; no manual page replacement
  • Batch processing — 500 renewal policies generated overnight; each customized with customer name, coverage amounts, dates; ops team reviews, not creates
  • Template marketplace — legal team creates templates with placeholder fields; operations fills them with data; templates versioned and reusable across products

Related Skills

  • stirling-pdf — choose it when you would rather deploy a ready-made self-hosted service than maintain your own PDF code: its HTTP API already covers merging, form filling, watermarking and certificate signing

Related use cases