Architecture Teardown Puppeteer vs Typst ISO 19005-3 ZUGFeRD 2.2 September 2026 • 7 min read • by Rubrol Engineering Team

Why Puppeteer Cannot Generate Legal EN 16931 E-Invoices (And What to Use Instead)

Every engineering team on earth starts with the same lazy reflex: "We'll just write HTML/Tailwind and call Puppeteer page.pdf()." It works for a while - until you encounter European B2B compliance. Here is the technical breakdown of why Chromium fails the law, why your bill is 10x too high, and what to use instead.

The "It's Just HTML" Trap

For a decade, web developers treated PDF generation as a web-page printing problem. Gotenberg, Browserless, and self-rolled Docker images spinning up Headless Chrome became the default architecture for every SaaS generating invoices.

It was always a hack. Running a 500MB web browser with a full Blink rendering tree and a V8 JavaScript engine just to put four text strings and an invoice total onto an A4 page is insane.

In 2026, this hack officially dies.

With the rollout of mandatory electronic invoicing across the European Union (EN 16931, French Factur-X, German ZUGFeRD 2.2), your PDF is no longer just a visual document. It is a dual-layer cryptographic container.

And Puppeteer cannot build it. Here are the three fatal architectural reasons why.

The Legal Consequence

In France, Germany, and Poland, issuing an invalid electronic invoice can result in tax penalties of up to €15 per invoice (capped at €15,000 to €45,000 per year), plus enterprise accounting systems completely rejecting payment.

1. Chrome Has No Concept of ISO 19005-3 (PDF/A-3b)

Factur-X mandates that the visual invoice container conform strictly to ISO 19005-3 (PDF/A-3b). This standard exists so that a document can be archived for 10+ years without relying on external fonts, system colors, or dynamic browser scripts.

When you call page.pdf() in Puppeteer:

2. The Root Catalog Attachment Disaster (`/AFRelationship`)

Under EN 16931, the machine-readable UN/CEFACT CII XML (factur-x.xml) cannot just be an ordinary file attachment like a zip file.

It must be attached at the PDF Catalog Root with a strictly defined Associated File (`/AF`) relationship dictionary set to /AFRelationship /Alternative:

<<
  /Type /Filespec
  /F (factur-x.xml)
  /UF (factur-x.xml)
  /AFRelationship /Alternative
  /Desc (Factur-X electronic invoice)
  /EF << /F 12 0 R /UF 12 0 R >>
>>

Puppeteer cannot touch low-level PDF dictionary objects. To work around this, developers build horrific Franken-stacks:

  1. Run Chromium to render HTML to a dirty PDF (takes 1,200ms).
  2. Shell out to pdftk or qpdf to inject attachments (takes 300ms).
  3. Run a Python script to patch the XMP metadata schema (takes 200ms).

Your latency is now 1.8 seconds per invoice. Your Docker image weighs 1.4 Gigabytes. And when 50 customers purchase at the same time, your EC2 instance runs out of memory and crashes.

3. The Compute Cost: Why You Are Burning \$500/Month on Nothing

Let's look at the cold, hard numbers comparing Puppeteer clusters against a native compiled engine like Rubrol:

Metric Headless Chrome / Puppeteer Rubrol (Typst Native)
Cold Start / Engine Init 800ms - 2,000ms Sub-150ms
Warm Render Latency 600ms - 1,200ms 5ms - 25ms (40x faster)
RAM per Process 450MB - 800MB (Chrome V8) < 28MB (In-process Rust)
Throughput per CPU Core ~1.2 invoices / sec ~85 invoices / sec
Native PDF/A-3b Support ❌ Impossible without post-processors ✅ Built-in turnkey compiler
EN 16931 Validation ❌ None (external tools needed) ✅ In-memory Schematron validation

What to Use Instead: The Rubrol Architecture

If you want sub-10ms performance and certified compliance, you need an engine designed from the ground up around Apache 2.0 Typst and native PDF packaging.

That is exactly what Rubrol is:

# Run the zero-dependency sidecar (multi-arch, ~30MB RAM footprint):
docker run -d -p 8080:8080 --name rubrol-sidecar ghcr.io/maxcomperatore/rubrol:latest

# Compile a certified Factur-X / ZUGFeRD PDF in 160ms:
curl -X POST http://localhost:8080/v1/facturx/render \
  -H "Content-Type: application/json" \
  -d '{
    "template": "b2b_invoice",
    "profile": "EN 16931",
    "data": {
      "invoice_number": "FA-2026-0842",
      "issued_date": "2026-09-17",
      "seller": { "name": "Acme Europe", "vat_id": "FR12345678901", "country": "FR" },
      "buyer": { "name": "Deutsche Cloud GmbH", "vat_id": "DE987654321", "country": "DE" },
      "line_items": [{ "name": "License", "qty": 1, "unit_price": 1000.00, "tax_rate": 0.20 }],
      "grand_total": 1200.00,
      "currency": "EUR"
    }
  }' --output invoice_facturx.pdf

No Chromium. No V8 garbage collector freezes. No page-break CSS bugs. Just clean, mathematical document layout that runs 40x faster on a fraction of the compute.

Kill Chromium in Your PDF Pipeline Today

Deploy the Rubrol sidecar into your cluster. Generate audit-proof Factur-X and B2B invoices at native machine speed.

Unlock Pro Templates & Compliance Suite →