Released docs. You are viewing the documentation published with v0.34.0. Development docs are available at Latest.
How Registry Render stays byte-stable
Registry Render promises that fixed inputs produce identical PDF bytes, on the same machine and across machines. This page explains the model behind that promise: which values fix the bytes, which mechanisms hold them fixed, and what a change to any of them means operationally. There is nothing to run here; read it to understand what you are trusting when you store a render’s hash.
What fixes a document
Section titled “What fixes a document”A rendered document is a function of five inputs: the sealed bundle, the document type, the
locale, the request, and the issuance time. The request is two parts, data (validated against
the document’s JSON Schema) and assets (base64 images decoded and served to the template as
virtual files). Nothing else enters: no host fonts, no environment, no machine clock.
The renderer builds one envelope holding those inputs plus the bundle’s label tables and the
document’s own version, canonicalizes it with RFC 8785 (JSON Canonicalization Scheme), and injects
the resulting string into the template as the single Typst input. The dataSha256 a caller stores
is the sha256 of exactly those injected bytes, so the hash, the template’s view of the request,
and the canonical envelope are one thing, not three.
issuedAt is the document’s issuance claim and the only clock. datetime.today() in a template
and the PDF creation date both derive from it, so a re-render with the same claim is identical and
a different claim produces different bytes. The instant is normalized to UTC: 10:32Z and
13:32+03:00 render identically, and the caller’s offset is deliberately not preserved. A
template that needs a local date shifts the instant itself, at full-second precision, with
datetime.today(offset: ...).
What holds the bytes fixed
Section titled “What holds the bytes fixed”Each render runs in a fresh Typst world and library, because a template’s inputs ride the library
and any reuse would leak state or memoized layout between requests. The world offers the template
exactly three roots: the bundle, the request’s decoded assets under a virtual assets/ namespace,
and the vendored package tree. There is no host font discovery and no network path, and path
containment is enforced by the world itself, so a template that passes request-controlled text to
image() still cannot read outside the bundle. Failure messages name virtual paths, never the
host’s directory layout.
Font order is fixed: the binary’s baseline set first, then the bundle’s fonts sorted by path, mirroring the book order the Typst CLI builds. Font order decides fallback, so it may never depend on filesystem iteration order.
The PDF metadata carries a fixed, version-free creator string, so the renderer version appears nowhere in the envelope or the PDF bytes. A Typst upgrade that changes no layout therefore changes no bytes.
Below the renderer sits the compression stack, and it is part of the contract: the workspace lockfile pins the Typst release’s own versions of the deflate stack, because a patch-level change there can re-encode a compressed stream and move every byte after it. An upgrade to Typst or to the deflate stack is a reviewed golden-hash change, never a silent pass.
The bundle is sealed before serving: the manifest lists every governed file with its sha256, and a served render re-verifies the seal per request, in the worker, not only at startup. A bundle that drifted, or whose seal was stripped while the service was running, is refused with a named problem and an audit event.
Where the proof runs
Section titled “Where the proof runs”Three example bundles, one per shape of document, are the acceptance fixtures. The golden test pins each bundle’s PDF hash, envelope hash, and file closure, and a two-OS job in CI re-runs it on Linux and macOS for every change to the renderer, its platform dependencies, or the bundles. Identical hashes across both operating systems are the cross-machine proof; a hash that moved is a review, not a failure to suppress.
What this means operationally
Section titled “What this means operationally”Store both hashes with the record the document was made from: pdfSha256 for the artifact,
dataSha256 for the exact request the artifact renders. A later re-render from the same stored
inputs reproduces the first byte for byte, which is what makes a dead-letter replay safe and what
lets an auditor compare the paper against the ledger. If a hash changes, one of the five inputs
changed, the bundle’s seal changed, or a dependency upgrade changed output, and the last case
arrives as a golden-hash review, not silently.