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
-
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. -
WeasyPrint — HTML/CSS to PDF. Python dependency. Good CSS support. No musl build (requires Cairo/Pango with glibc). Rendering inconsistencies across versions.
-
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.
-
Apache FOP — XSL-FO to PDF. Java dependency. Verbose XML authoring. Poor developer experience.
-
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!Sendisolation 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
RenderErrortypes with template path and line number context -
E2E tests verify PDF generation for all 14 SNAP notice templates