How to Handle PDF/A Archival Compliance in Your Document Pipeline

A practical guide for developers on generating PDF/A-compliant documents for long-term archival, including font embedding, color profiles, metadata, and validation.

pdfpdf/acompliancearchivalnode.jsdocument-generation

How to Handle PDF/A Archival Compliance in Your Document Pipeline

If your application generates contracts, invoices, medical records, or any document that needs to survive a decade or two of regulatory scrutiny, regular PDFs are not enough. You need PDF/A — an ISO-standardized subset of PDF designed for long-term preservation. The catch: getting PDF/A right is harder than it looks, and a "looks fine in Acrobat" PDF will fail compliance the moment an auditor runs it through veraPDF.

This post walks through what PDF/A actually requires, where most document pipelines break, and how to bake compliance into your generation flow.

What PDF/A actually requires

PDF/A is not a single format. The flavors that matter in practice:

  • PDF/A-1b — Visual reproducibility. The minimum bar. Based on PDF 1.4.
  • PDF/A-2b — Adds JPEG2000, transparency, layers. Based on PDF 1.7.
  • PDF/A-3b — Same as 2b, but allows embedded files (useful for ZUGFeRD/Factur-X e-invoicing).
  • The a variants (1a, 2a, 3a) additionally require tagged structure for accessibility.

Across all of them, you must satisfy roughly the same hard rules:

  1. All fonts must be embedded — no font substitution at viewing time.
  2. No encryption — the document must be readable forever without keys.
  3. Device-independent color — either use an output intent (ICC profile) or stick to calibrated color spaces.
  4. No external dependencies — no JavaScript, no audio/video, no references to external resources.
  5. Valid XMP metadata — including a pdfaid:part and pdfaid:conformance claim.
  6. No transparency (PDF/A-1 only) — alpha channels are forbidden in the 1.x line.

Skip any of these and a validator will reject the file.

Where pipelines typically break

Most teams generate PDFs from HTML using a headless browser or from a templating library. Both paths have predictable failure modes:

  • Web fonts loaded via @font-face from a CDN — the browser rasterizes them fine, but the resulting PDF may reference subsetted fonts without embedding the full encoding tables PDF/A requires.
  • CSS rgba() colors with alpha — produces transparency objects that violate PDF/A-1.
  • System fonts (Arial, Helvetica) — often referenced by name without being embedded.
  • Missing XMP packet — even a perfectly rendered PDF will fail without the <rdf:Description> block declaring conformance.
  • sRGB color used without an output intent — flags color management warnings.

A working approach

Here is the pattern I recommend regardless of which library you use:

Step 1: Lock down fonts. Self-host your fonts and explicitly embed them. If you are using Puppeteer or Playwright, preload the font files locally and reference them by file:// URLs.

ts
await page.addStyleTag({ content: ` @font-face { font-family: "Inter"; src: url("file:///app/fonts/Inter-Regular.ttf") format("truetype"); font-weight: 400; } body { font-family: "Inter", sans-serif; } `, });

Step 2: Strip transparency. Audit your CSS for rgba(), opacity, and box-shadow. Convert them to flat equivalents if you are targeting PDF/A-1.

Step 3: Inject an output intent and XMP metadata. This is the step nearly every homegrown solution gets wrong. You need to post-process the generated PDF to attach an ICC profile (typically sRGB IEC61966-2.1) and write the XMP packet.

Using pdf-lib plus a separate tool like Ghostscript:

bash
gs -dPDFA=2 \ -dBATCH \ -dNOPAUSE \ -sColorConversionStrategy=UseDeviceIndependentColor \ -sDEVICE=pdfwrite \ -sPDFACompatibilityPolicy=1 \ -sOutputFile=output-pdfa.pdf \ PDFA_def.ps input.pdf

You'll need a PDFA_def.ps file that references your ICC profile. Ghostscript ships an example you can adapt.

Step 4: Validate. Never trust your own pipeline. Run every output through veraPDF before considering the file compliant.

bash
verapdf --flavour 2b --format text output-pdfa.pdf

A passing run looks like this:

txt
PASS output-pdfa.pdf

Anything else, including warnings, should fail your CI build.

Doing this with Kamy

If you'd rather not maintain a Ghostscript install, font catalog, and ICC profile in your Docker image, Kamy can render directly to PDF/A. You pass a conformance option and the API handles font embedding, output intent, and metadata injection:

ts
import { kamy } from "@kamydev/sdk"; const pdf = await kamy.documents.render({ template: "patient-record", data: { patientId: "P-9281", visitDate: "2025-03-14" }, output: { format: "pdf", conformance: "PDF/A-2b", }, }); console.log(pdf.url);

The returned file is already validated against veraPDF on our side, but you should still run validation as part of your own pipeline if you have hard regulatory requirements.

Archive-side concerns

Compliance does not end at generation. A few things to plan for:

  • **Storage imm