Rendering PDFs in pure Rust with typst (and why we dropped headless Chrome)

Engineering

Rendering PDFs in pure Rust with typst (and why we dropped headless Chrome)

FastYoke Engineering · 8 min read · Aug 24, 2026

  • Architecture
  • PDF
  • Rust

Someone signs a waiver, and a PDF has to exist

A customer fills out a public form — an intake, a release, a liability waiver — and hits submit. A second later, a styled PDF of that submission has to exist: laid out, paginated, with the right fonts, ready to file, email, or hand to an auditor. That is one of the most ordinary things a business application does. It is also where a surprising number of stacks reach for a whole web browser to get the job done.

The standard playbook is HTML-to-PDF: render your document as an HTML page, run it through headless Chrome or Puppeteer, and print to PDF. It works, and it is genuinely the path of least resistance if you already have the markup. We looked hard at it, and chose not to depend on it. FastYoke renders documents to PDF in pure Rust, in-process, with no browser anywhere in the picture. This post is about why, and what we use instead.

What a browser actually costs you on the server

Headless Chrome is a remarkable piece of engineering. It is also a web browser, and a browser is a strange thing to ship inside a server whose job is to stamp a waiver into a PDF.

Consider what you are taking on. The Chromium binary is a hundreds-of-megabytes dependency — it dwarfs the actual application in most images, and it has to be present in every environment the app runs in. It runs as a separate process you now have to launch, supervise, and reap; a wedged render leaves a zombie holding a chunk of RAM. It is a memory hog by design, because it was built to run rich interactive pages, not to lay out a one-page form. And its output is not deterministic across versions: bump the Chrome version and the same document can paginate a hair differently, which is exactly the kind of drift you do not want in a signed record.

Then there is the security surface. To render your document, you are running a full browser engine — a JavaScript runtime, a layout engine, a font stack, image decoders — over content that, on a public form, originated with an untrusted submitter. That is a large and famously attacked surface to keep patched. Every Chrome security advisory becomes something you have to track, because you are shipping the browser.

For us the deciding factor was deployment. FastYoke runs in our cloud, but it also runs inside a customer's own data center, on an edge gateway, and in fully air-gapped installations with no outbound network. In those environments, "just install Chrome and keep it patched" is not a footnote — it is an ongoing operational burden the customer inherits. A single-binary deployment that suddenly needs a browser subprocess and a fonts package is not a single-binary deployment anymore.

You don't need a DOM to make a PDF

Here is the misconception worth correcting: rendering a document to PDF and rendering a web page are not the same problem, and the second is enormously more work than the first.

What a document PDF actually needs is a typesetting engine: deterministic layout, real font handling and shaping, pagination, tables, headers and footers, page numbers. What it does not need is a JavaScript engine, a DOM, a CSS cascade with decades of edge cases, or an event loop. Headless Chrome brings all of that along because it has to — it is a browser. When your input is a structured form submission and your output is a tidy paginated document, most of the machine is dead weight.

Strip the requirement down to "lay out a document deterministically and emit a PDF," and a browser is wildly overpowered for it. That reframing is what made a different tool obvious.

The tool: typst, in-process, in Rust

We render documents with typst, a modern typesetting engine — think of it as a contemporary, programmable take on what TeX does, built in Rust. It is not a browser and it does not pretend to be. You author a template in typst's markup, feed it your data, and it produces a PDF.

The properties that matter for a server:

  • Pure Rust, embeddable in-process. typst links into the same binary as the rest of the engine. There is no subprocess to launch or supervise, no browser to install, no separate fonts package to ship. The single binary stays a single binary.
  • Deterministic. The same template and the same data produce the same bytes. For a signed waiver or a filed intake form, a reproducible artifact is not a nice-to-have — it is the point.
  • Purpose-built for documents. Pagination, tables, font shaping, and headers are first-class, not things you coax out of a print stylesheet.
  • A small, understood surface. A typesetting engine over a fixed template is a far narrower thing to reason about than a full browser rendering arbitrary HTML.

A typst template reads roughly like this — a document, not a web page:

#set page(paper: "us-letter", numbering: "1 / 1")
#set text(font: "Inter", size: 11pt)

= Liability Release

Signed by *#name* on #date.

#table(
  columns: 2,
  [Participant], [#name],
  [Date],        [#date],
)

You bind name and date from the form submission, compile, and you have a PDF — no browser, no network, no separate process.

Two jobs, both pure Rust: layout vs. filling existing PDFs

There is a second half to PDF work, and it is worth being precise about, because it is a genuinely different job from the one above.

typst is the layout-and-render path: you have structured data and you generate a document from a template. But plenty of the time, the PDF already exists — a government form, a standardized release, a fillable template someone hands you — and the task is to fill in its fields, not to lay out a new document. That is a distinct problem, and we solved it as a separate, open-source Rust crate: fastyoke-pdf-forms.

It is, in our own words, "extracted from the FastYoke e-signature and forms pipeline," and it does the low-level PDF work: it fills AcroForm fields in an existing PDF, flattens widgets so the filled values become permanent, composites overlays, and stamps signatures. It is built on lopdf — a Rust PDF library — and, like everything else here, does its work in-process with no browser involved.

So the split is clean:

  • Generating a document from data → typst lays it out and renders it.
  • Filling or stamping a PDF that already exists → the fastyoke-pdf-forms / lopdf path manipulates it directly.

Two different jobs, two different tools, one shared property: both are pure Rust, both run in the same process, and neither one needs Chrome. The e-signature flow leans on both — generate the document, then seal it — without a browser touching the page at any step.

Why "pure Rust, in-process, deterministic" is the whole ballgame

Those three words are not aesthetic preferences. They are the properties that let this run where our customers need it to run.

Because rendering is in-process, there is no subprocess to supervise and no browser binary bloating the image — the deployment stays a single artifact. Because it is pure Rust with no ambient capabilities, it needs no network and no filesystem access to do its job, which is exactly what makes it safe on an air-gapped VM with no outbound and nothing to phone home to. And because it is deterministic, the PDF a customer generates is a reproducible record — the same inputs yield the same document every time, which is what you want the moment a signature is attached to it.

Put plainly: all the PDF compute runs on the tenant's own VM, with no external rendering service and no browser to keep patched. That is a promise you simply cannot make when a headless browser sits in the critical path.

The honest tradeoffs

This is not free, and it would be dishonest to pretend otherwise.

typst is a typesetting language, not HTML. You author templates in it, and if your team's mental model is HTML and CSS, there is a real learning curve. It is a different way of thinking about a document.

It is newer and less battle-tested than a browser. Chromium has had an enormous number of engineer-years and a planet's worth of pages thrown at it. For exotic or highly unusual layouts, a browser has simply seen more edge cases. typst is excellent and improving fast, but it is younger.

Existing HTML designs don't port for free. If you already have a pixel-perfect HTML invoice built over years, you are not dropping it into typst unchanged — you are rebuilding it in a different markup. For a team whose documents are all already HTML, that cost is real and worth weighing.

Our take is that for the documents a business application actually generates — forms, waivers, releases, intakes, receipts — the tradeoff is heavily worth it. The layouts are structured and repeatable, exactly what a typesetting engine is good at, and the deployment and security wins are decisive. If your product's core value were rendering arbitrary third-party HTML pixel-for-pixel, you might weigh it differently. Ours isn't, so we didn't.

Where this goes

The pattern generalizes past forms. Any document FastYoke needs to emit — an e-signature record, an exported report, a generated statement — rides the same pure-Rust path, and inherits the same properties: in-process, deterministic, no browser, runs anywhere the engine runs. That the e-signature pipeline and the open-source forms crate share a rendering stack is not a coincidence; it is the payoff of not having a browser in the middle.

If you want the deployment side of this story, see how the same engine runs on your own hardware in On-Prem and the runtime tiers. The forms-to-PDF flow itself is on Free Forms, the crate is part of our open-source program, and the sealing step is covered in Adding e-signatures to any workflow. For the sandbox that runs tenant logic under the same no-browser, runs-anywhere philosophy, see why we chose WebAssembly.