ADR-010: Typst for Document Generation

On this page

Context

Canopy generates legally significant documents: notices of action (approval, denial, termination), hearing rights, verification checklists, renewal forms, and federal reports. These must be:

  • Pixel-reproducible (same input = identical PDF, byte-for-byte)

  • Accessible (tagged PDF for screen readers)

  • Jurisdiction-customizable (letterhead, addresses, legal citations vary by state)

  • Fast (sub-second generation for interactive use)

  • Compilable on musl/Alpine (no glibc dependency)

Options Considered

  1. LaTeX — gold standard for typesetting. Massive installation footprint (~4 GB TeX Live). Slow compilation. Arcane macro language. No native Rust integration. Requires shelling out to pdflatex/lualatex.

  2. WeasyPrint — HTML/CSS to PDF. Python dependency. Good CSS support. No musl build (requires Cairo/Pango with glibc). Rendering inconsistencies across versions.

  3. Headless Chrome / Puppeteer — HTML to PDF via browser. Huge runtime (~400 MB Chromium). Non-deterministic rendering (font hinting, anti-aliasing). Security surface area of a full browser engine.

  4. Apache FOP — XSL-FO to PDF. Java dependency. Verbose XML authoring. Poor developer experience.

  5. Typst — modern typesetting system written in Rust. Native library embedding via typst-as-lib. Sub-second compilation. Clean markup language. Pure Rust (musl-compatible). Deterministic output. Active development with growing ecosystem.

Decision

Option 5: Typst via typst-as-lib.

  • crates/canopy-typst/ wraps Typst with a dedicated OS render thread (same !Send isolation pattern as zen-engine in canopy-rules)

  • Templates live in rulesets/{jurisdiction}/notices/ following ADR-003 (ruleset-as-data) and ADR-006 (jurisdiction-agnostic organization)

  • Orchard design system provides shared components (components/orchard.typ) for consistent branding across jurisdictions

  • Template manifest (manifest.toml) maps notice types to template files with version tracking

Consequences

Positive

  • Pure Rust: compiles to musl static binary, runs in Alpine containers with zero external dependencies

  • Sub-second PDF generation (~50-200ms for a typical notice) — suitable for interactive use

  • Deterministic output: same template + same data = identical PDF (testable in CI)

  • Clean template language accessible to non-developers (policy staff can review notice wording)

  • Jurisdiction customization via template directory structure (ADR-006)

  • Native font embedding (Montserrat bundled in rulesets/georgia/notices/fonts/)

Negative

  • Typst is younger than LaTeX — smaller ecosystem, fewer examples, still evolving

  • Tagged PDF (accessibility) support is in progress upstream, not yet production-ready

  • Learning curve for team members familiar with HTML/CSS but not typesetting markup

  • Template errors produce Typst-specific error messages that may be unfamiliar

Mitigations

  • Orchard component library abstracts common patterns (letterhead, footer, tables) so most template work is data binding, not layout

  • canopy-typst wraps Typst errors into structured RenderError types with template path and line number context

  • E2E tests verify PDF generation for all 14 SNAP notice templates

Edit this page · default