# OpenSpec Change: enterprise-server-pdf - **Change ID**: enterprise-server-pdf - **Package**: `@suaveplan/pdf` - **Category**: server/services - **Target path**: `server/services/pdf` - **Status**: PROPOSED - **Date**: 2026-04-24 - **Reference spec**: `/mnt/s/web/suaveplan/enterprise/pdf.md` --- ## Summary Deliver `@suaveplan/pdf` on the genesis branch from scratch at enterprise quality. The package provides server-side PDF generation for receipts, reports, and invoices. It exposes a `PdfBuilder` with a fluent page/section/element composition API and a pluggable `PdfRenderer` interface, shipping two concrete renderers: a `PuppeteerRenderer` (HTML → PDF via headless Chrome) and a `PdfKitRenderer` (pure JS, no browser dependency). Zod-validated options, `@suaveplan/error` codes, OTel spans per render, `@suaveplan/templates` for HTML template rendering, and `AbortSignal` support for long renders. --- ## Current-state audit (genesis) Does not exist on genesis — fresh build. --- ## Available workspace deps on genesis | Package | Role | |---|---| | `@suaveplan/error` (`workspace:^`) | Typed `PDF.*` error codes | | `@suaveplan/types` (`workspace:^`) | `LoggerLike` | | `@suaveplan/templates` (`workspace:^`) | HTML template rendering | | `@suaveplan/logger` (`workspace:^`) | Structured logs | | `zod` (`catalog:forms`) | Options validation | | `@opentelemetry/api` (`catalog:observability`) | Spans + counters + histograms | | `puppeteer-core` (`catalog:services`) | Headless Chrome (optional renderer) | | `pdfkit` (`catalog:services`) | Pure-JS PDF generation (optional renderer) | | `tslib` (`catalog:build`) | Runtime helpers | --- ## Public API surface ```ts interface PdfRenderer { render(document: PdfDocument, options?: RenderOptions): Promise dispose(): Promise } class PdfBuilder { page(options?: PageOptions): this text(content: string, options?: TextOptions): this heading(content: string, level?: 1 | 2 | 3): this table(data: TableData, options?: TableOptions): this image(src: string | Uint8Array, options?: ImageOptions): this spacer(height?: number): this footer(content: string): this header(content: string): this build(): PdfDocument } function createPdfBuilder(): PdfBuilder function createPuppeteerRenderer(options?: PuppeteerRendererOptions): PdfRenderer function createPdfKitRenderer(options?: PdfKitRendererOptions): PdfRenderer interface PdfDocument { pages: PdfPage[] metadata: PdfMetadata } interface PdfMetadata { title?: string author?: string subject?: string keywords?: string[] creator?: string } interface RenderOptions { signal?: AbortSignal format?: 'A4' | 'Letter' | 'Legal' landscape?: boolean margins?: { top: number; right: number; bottom: number; left: number } } type PdfErrorCode = | 'PDF_RENDER_FAILED' | 'PDF_TIMEOUT' | 'PDF_INVALID_CONTENT' | 'PDF_RENDERER_UNAVAILABLE' function registerPdfErrorCodes(): void ``` --- ## File layout ```text server/services/pdf/ package.json tsconfig.json vitest.config.ts src/ index.ts error-codes.ts constants/index.ts types/ pdf.types.ts pdf.types.md builder/ pdf-builder/ pdf-builder.ts pdf-builder.md pdf-builder.types.ts pdf-builder.test.ts index.ts renderers/ puppeteer-renderer/ puppeteer-renderer.ts puppeteer-renderer.md puppeteer-renderer.types.ts puppeteer-renderer.test.ts index.ts pdfkit-renderer/ pdfkit-renderer.ts pdfkit-renderer.md pdfkit-renderer.types.ts pdfkit-renderer.test.ts index.ts tests/ integration.test.ts ``` --- ## Impact Downstream: invoice generation, report exports, document archiving. Server-only. **Prerequisites on Verdaccio**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/templates`, `@suaveplan/logger`.