Skip to content
Registry StackDocsDevelopment (unreleased)

How Registry Render stays byte-stable

View as Markdown

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.

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: ...).

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.

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.

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.