Accrue renders invoice PDFs from the same Accrue.Invoices.Components that power the transactional emails, but the default invoice renderer is now native Rendro rather than Chrome. The invoice entry point is Accrue.Invoices.render_invoice_pdf/2, which resolves :invoice_pdf_adapter and renders a PDF without requiring a browser process by default.

The older Accrue.PDF behaviour still exists for HTML-to-PDF adapters such as ChromicPDF or a custom Gotenberg sidecar, but it is no longer the primary invoice path.

If you only read one section: Rendro is the default. Jump to ChromicPDF explicit compatibility path only if you explicitly want the old HTML-based path.

Adapters

Invoice rendering ships with three first-party adapters:

AdapterWhen to useReturns
Accrue.InvoiceRenderer.RendroProduction default. Native Elixir invoice PDF rendering with no Chrome dependency.{:ok, pdf_binary}
Accrue.InvoiceRenderer.ChromicPDFOptional fallback. Preserves the older HTML → Chrome path via a host-supervised ChromicPDF pool.{:ok, pdf_binary}
Accrue.InvoiceRenderer.NullPDF-disabled / Chrome-hostile deploys. Returns a typed error without rendering.{:error, %Accrue.Error.PdfDisabled{}}

The invoice renderer is resolved via :invoice_pdf_adapter:

# config/config.exs
config :accrue, :invoice_pdf_adapter, Accrue.InvoiceRenderer.Rendro

# config/test.exs
config :accrue, :invoice_pdf_adapter, Accrue.InvoiceRenderer.Test

If you still need the lower-level HTML seam, :pdf_adapter continues to configure Accrue.PDF for ChromicPDF/custom HTML renderers.

Rendro default

The default path needs no extra supervisor child and no Chrome/Chromium binary on the host image. That keeps the default install/setup smaller and easier to maintain.

The main tradeoff is honesty about assets and fonts:

  • remote logo_url fetching is not part of the Rendro default path
  • unsupported glyphs fail explicitly instead of silently degrading
  • lazy render semantics are unchanged; Accrue still re-renders from current invoice data unless you explicitly store the bytes yourself

ChromicPDF explicit compatibility path

If you want the previous HTML-based invoice rendering path, switch:

config :accrue, :invoice_pdf_adapter, Accrue.InvoiceRenderer.ChromicPDF

Accrue still does not start ChromicPDF itself. The host app owns the supervision tree and supervises the pool.

This is an explicit compatibility path. Invoice rendering only switches when you set :invoice_pdf_adapter; Accrue does not infer invoice behavior from the lower-level :pdf_adapter HTML seam.

# lib/my_app/application.ex
children = [
  MyApp.Repo,
  {ChromicPDF, on_demand: true},
  MyAppWeb.Endpoint
]

Keep accrue_mailers queue concurrency less than or equal to the ChromicPDF pool size if you attach invoice PDFs from mailer jobs.

If this explicit compatibility path is configured without a running ChromicPDF process, Accrue.Invoices.render_invoice_pdf/2 returns {:error, %Accrue.Error.InvoiceRendererUnavailable{adapter: Accrue.InvoiceRenderer.ChromicPDF, reason: :chromic_pdf_not_started}}.

Migration

The seam split is explicit:

  • :invoice_pdf_adapter owns invoice rendering.
  • :pdf_adapter remains the lower-level Accrue.PDF HTML seam.

Invoice rendering does not infer behavior from :pdf_adapter. If you are upgrading, use the host state that matches your app:

1. No custom PDF config

If you never customized Accrue's PDF settings, no action needed. Rendro is now the default invoice renderer, so invoice PDFs render without Chrome on the normal path.

2. You only set :pdf_adapter

If your host only set config :accrue, :pdf_adapter, ..., invoice PDFs no longer follow that key. Set :invoice_pdf_adapter explicitly if you want the legacy Chrome-backed invoice path:

config :accrue, :invoice_pdf_adapter, Accrue.InvoiceRenderer.ChromicPDF

Keep :pdf_adapter only if you also still use the lower-level HTML seam.

3. You use a custom HTML seam

If your host uses a custom Accrue.PDF adapter, keep :pdf_adapter for that HTML seam. Invoice rendering remains on the default Rendro path unless you also set :invoice_pdf_adapter explicitly.

That means a host can keep a custom HTML renderer for non-invoice callers without changing invoice behavior, and a host can choose Accrue.InvoiceRenderer.ChromicPDF only when it intentionally wants the old invoice path back.

Null graceful degradation {#null-adapter}

Accrue.InvoiceRenderer.Null is the escape hatch for PDF-disabled deploys. It returns a typed error without rendering:

iex> Accrue.Invoices.render_invoice_pdf(invoice)
{:error, %Accrue.Error.PdfDisabled{reason: :adapter_disabled, docs_url: "..."}}

The mailer path treats %Accrue.Error.PdfDisabled{} as terminal and falls through to the hosted invoice URL instead of retrying forever.

@page CSS warning (Chromic fallback only)

ChromicPDF does not interpret @page CSS rules. Setting page size, margins, or paper dimensions via a stylesheet has no effect — the output will silently use ChromicPDF's defaults.

Wrong:

/* Ignored by ChromicPDF — do NOT do this. */
@page {
  size: A4 portrait;
  margin: 20mm 15mm;
}

Right: pass paper options through the adapter opts:

Accrue.PDF.render(html,
  size: :a4,
  paper_width: 8.27,
  paper_height: 11.69,
  margin_top: 0.5,
  margin_bottom: 0.5,
  margin_left: 0.4,
  margin_right: 0.4
)

For explicit page breaks inside content, use the CSS page-break-* properties (page-break-before: always; works as expected inside the printed document, just not @page at the top level).

Font strategy

Webfonts via <link rel="stylesheet" href="https://fonts.googleapis.com/..."> are unreliable under headless Chromium — the fetch may race the render deadline. The recommended pattern is to base64-embed the font bytes directly into the HTML via @font-face src: url(data:...):

<style>
  @font-face {
    font-family: "Inter";
    font-weight: 400;
    src: url(data:font/woff2;base64,d09GMgABAAAAA...) format("woff2");
  }
  body { font-family: "Inter", sans-serif; }
</style>

Keep the embedded font files small — a single weight is usually enough for an invoice. If you do not need a custom typeface, the default Accrue.Config.branding/0 :font_stack (-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif) renders cleanly on every platform Chromium ships on, with zero embedding overhead. That is the recommended default for v1.0.

See also