# PDF Stack Overhaul — Design
**Date:** 2026-05-12
**Branch:** `feat/documents-folders`
**Status:** Design approved; pending user review of spec; implementation planned via writing-plans skill.
## Goal
Replace `pdfme` (3 deps, 8 hand-coded coordinate templates, 571-line TipTap-to-pdfme bridge) with `@react-pdf/renderer` (JSX components, real layout primitives). Add `unpdf` for berth-PDF tier-2 rasterization. Add port-level logo upload with quality safeguards. Migrate only the internal-only PDF surfaces; remove invoice and admin-TipTap PDF generation entirely (they violate the new "no client-facing CRM-generated PDFs" rule).
## Scope (locked)
### KEEP & migrate to `@react-pdf/renderer` (internal-only)
| Surface | Current location | Caller |
| ----------------------- | --------------------------------------------------------------------------------------------------- | ------------------------------------- |
| Activity report | `src/lib/pdf/templates/reports/activity-report.ts` | `src/lib/services/reports.service.ts` |
| Revenue report | `src/lib/pdf/templates/reports/revenue-report.ts` | same |
| Pipeline report | `src/lib/pdf/templates/reports/pipeline-report.ts` | same |
| Occupancy report | `src/lib/pdf/templates/reports/occupancy-report.ts` | same |
| Client summary export | `src/lib/pdf/templates/client-summary-template.ts` | `src/lib/services/record-export.ts` |
| Berth spec export | `src/lib/pdf/templates/berth-spec-template.ts` | same |
| Interest summary export | `src/lib/pdf/templates/interest-summary-template.ts` | same |
| Expense sheet | `src/lib/services/expense-pdf.service.ts` (currently uses pdfme indirectly via `expense-export.ts`) | same |
### REMOVE entirely
| Removal | Reason |
| ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `src/lib/pdf/templates/invoice-template.ts` + `generatePdf` call in `invoices.ts:604` + API route `/api/v1/invoices/[id]/generate-pdf` | Invoices are client-facing; no CRM-generated client-facing PDFs. Future invoice rendering will use the deferred AcroForm-fill admin-template feature. |
| `src/lib/pdf/tiptap-to-pdfme.ts` (571 lines) + API route `/api/v1/admin/templates/preview` + `generatePdf` block in `document-templates.ts:516` | TipTap document templates are Documenso seed bodies; CRM does not render them to PDF anymore. |
| `src/lib/pdf/templates/eoi-standard-inapp.ts` (337 lines, HTML seed) + seed-data references | Only used as the seed `bodyHtml` text on a `document_templates` row. The in-app EOI is rendered by `fill-eoi-form.ts` (pdf-lib), not from this HTML. Safe to drop. |
| `src/lib/pdf/generate.ts` (24 lines) | Pdfme wrapper; replaced by `src/lib/pdf/render.ts`. |
| Deps: `@pdfme/common`, `@pdfme/generator`, `@pdfme/schemas` | Replaced by `@react-pdf/renderer`. |
### STAYS UNTOUCHED
- `src/lib/pdf/fill-eoi-form.ts` (pdf-lib AcroForm fill on `assets/eoi-template.pdf`) — the in-app EOI pathway.
- `src/lib/services/berth-pdf-parser.ts` tier-1 (pdf-lib AcroForm read) and tier-3 (AI fallback). Tier-2 (Tesseract OCR) gets `unpdf` for PDF→image rasterization.
- `pdf-lib` dep (still needed by `fill-eoi-form.ts` and `berth-pdf-parser.ts`).
- All Documenso integration code.
## Architecture
Three orthogonal PDF paths post-migration, each with a single owner:
```
┌──────────────────────────┐ ┌──────────────────────────┐ ┌────────────────────────┐
│ react-pdf (this phase) │ │ pdf-lib AcroForm fill │ │ Documenso (external) │
│ Internal only │ │ Standardized + signing │ │ Client-facing signed │
│ │ │ │ │ docs │
│ • Reports (×4) │ │ • In-app EOI │ │ │
│ • Expenses │ │ • Future admin-upload │ │ (handled outside our │
│ • Record exports (×3) │ │ invoice templates │ │ system) │
│ • Future internal lists │ │ (deferred) │ │ │
└────────────┬─────────────┘ └────────────┬─────────────┘ └────────────────────────┘
│ │
▼ ▼
src/lib/pdf/render.ts src/lib/pdf/fill-eoi-form.ts
(renderToBuffer + (unchanged this phase)
renderToStream)
│
▼
src/lib/pdf/brand-kit/
├─ DocumentShell.tsx
├─ Header.tsx
├─ Footer.tsx
├─ DataTable.tsx
├─ KeyValueGrid.tsx
├─ Section.tsx
├─ Badge.tsx
├─ charts/{Bar,Line,Pie,Funnel}Chart.tsx
├─ tokens.ts
└─ logo.ts
```
### Module boundaries
- **`brand-kit/`** — pure presentation primitives. No DB access, no CRM domain knowledge. Each component has typed props and renders react-pdf elements.
- **`templates/`** — one `.tsx` per document type. Imports brand-kit primitives + receives typed data props. No DB access; data fetching stays in the calling service.
- **`render.ts`** — the only module that touches `@react-pdf/renderer`'s `renderToBuffer` / `renderToStream`. Services call `renderPdf()` or `renderPdfStream()`.
- **`logo.ts`** — `resolvePortLogo(portId)` reads `system_settings.port_logo_file_id` and returns `{ source, buffer, mimeType }`. Cached per request via React `cache()`.
- **Chart rendering** — pure SVG components emitting react-pdf's native `