Skip to content
Registry StackDocsv0.46.0

Render your first document

For the operator

View as Markdown

You will render one letter with Registry Render twice, once at a terminal and once over HTTP, and prove the two PDFs are the same bytes.

Outcome
One letter rendered offline and by a local service, with identical PDF bytes.
Time
About 15 minutes, plus the build
Level
Local evaluation only
Prerequisites
A Registry Stack checkout and the Rust toolchaincurl, jq, and opensslA free loopback port 64123

Releases publish a Linux amd64 binary, which Deploy Registry Render installs. This tutorial builds from the checkout so it also works on macOS. From the checkout’s root:

Terminal window
. scripts/cargo-runtime-library-path.sh
registry_cargo_build "$PWD" --release --locked -p registry-render
export PATH="$PWD/target/release:$PATH"

The helper also sets the library path the binary needs on macOS, so run every registry-render command in this terminal. Change to an empty directory in it, then:

Terminal window
registry-render --version
registry-render <version> (typst 0.15.1)

Scaffold a bundle:

Terminal window
registry-render init render-work/bundle

The bundle holds manifest.yaml, which declares one document type, letter; its Typst template; a JSON Schema for its data; an English label table; fonts; and a fixture. init also prints a first-render command; this one gives paths relative to this directory and the issuance time the rest of the tutorial uses:

Terminal window
registry-render compile \
--bundle render-work/bundle \
--type letter \
--data render-work/bundle/fixtures/data.json \
--issued-at 2026-09-16T10:32:00Z \
--output render-work/letter.pdf
wrote render-work/letter.pdf (13292 bytes, pdf sha256 1e2dc9c981d19ef62549c3100395a106bc8fb30e01bef21c26844e45a0c29f9f, data sha256 685d9fbcaac74fea3e7ea7839e79da9f0ceb56e9cf218f9ae1fe9d0d0d09e9fa)

--issued-at is the document’s issuance time and the only clock the template sees, so your hashes match these. The pdf sha256 identifies the file; the data sha256 identifies the exact input it was rendered from.

The service serves only a package: a copy of the bundle with a checksum file. Build one and check it:

Terminal window
registry-render package \
--bundle render-work/bundle \
--output render-work/package \
--revision tutorial-1
registry-render check --bundle render-work/package
packaged render-work/package as sha256:<package-digest>
document letter v1 entry templates/letter.typ labels [en] pdf plain
bundle render-work/package v1 hash <digest-prefix> (19 fonts, 1 documents, 9 governed files)
0 errors, 0 warnings in 3 files

The sha256: digest names this package. A later change to any file in it is refused by name.

Create the API key callers present, and a runtime file that names the package, the key, and an audit file. Its paths are absolute:

Terminal window
mkdir -p render-work/deploy
openssl rand -hex 32 > render-work/deploy/api.key
chmod 600 render-work/deploy/api.key
here="$(pwd -P)/render-work"
cat > render-work/deploy/runtime.yaml <<EOF
apiVersion: id.registrystack.org/formats/render/runtime/v1alpha1
kind: RenderRuntimeConfig
listener:
bind: 127.0.0.1:64123
package:
root: $here/package
secretProviders:
file:
root: $here/deploy
authentication:
apiKeyRef: secret:file/api.key
audit:
path: $here/audit/render.jsonl
EOF
registry-render check --runtime-config "$here/deploy/runtime.yaml"
0 errors, 0 warnings in 1 file

Start the service and leave it running in this terminal:

Terminal window
registry-render serve --runtime-config "$here/deploy/runtime.yaml"

It logs JSON lines to standard error. Open a second terminal in the same directory and run the rest of the tutorial there:

Terminal window
curl -s http://127.0.0.1:64123/health
{"bundleHash":"<package-digest>","bundleVersion":1,"rendererVersion":"<version> (typst 0.15.1)","status":"ok","typstPin":"0.15.1"}

bundleHash is the package digest without sha256:, so you can tell which package is live.

Send the fixture data with the same issuance time. The key goes in a header file, which keeps it out of curl’s command line, where ps would show it to other local users:

Terminal window
(umask 077; printf 'Authorization: Bearer %s\n' "$(cat render-work/deploy/api.key)" \
> render-work/deploy/auth.header)
jq -c '{issuedAt: "2026-09-16T10:32:00Z", data: .}' render-work/bundle/fixtures/data.json \
> render-work/request.json
curl -s -X POST http://127.0.0.1:64123/v1/render/letter \
-H @render-work/deploy/auth.header \
-H "Content-Type: application/json" \
-d @render-work/request.json \
-o render-work/served.pdf -D render-work/headers.txt
grep -i '^x-registry' render-work/headers.txt
x-registry-pdf-digest: sha256:1e2dc9c981d19ef62549c3100395a106bc8fb30e01bef21c26844e45a0c29f9f
x-registry-data-digest: sha256:685d9fbcaac74fea3e7ea7839e79da9f0ceb56e9cf218f9ae1fe9d0d0d09e9fa
x-registry-pdf-sha256: 1e2dc9c981d19ef62549c3100395a106bc8fb30e01bef21c26844e45a0c29f9f
x-registry-data-sha256: 685d9fbcaac74fea3e7ea7839e79da9f0ceb56e9cf218f9ae1fe9d0d0d09e9fa
x-registry-warning-count: 0
x-registry-document-version: letter v1

The headers carry the two hashes compile printed, as labelled digests and as the deprecated bare hex, and the warning count. Compare the files themselves:

Terminal window
cmp render-work/letter.pdf render-work/served.pdf && echo identical
identical

Two processes rendered the letter, and the bytes are the same.

A wrong key, then a request without an issuance time:

Terminal window
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://127.0.0.1:64123/v1/render/letter \
-H "Authorization: Bearer wrong-key-0000000000000000000000000000" \
-H "Content-Type: application/json" \
-d @render-work/request.json
curl -s -X POST http://127.0.0.1:64123/v1/render/letter \
-H @render-work/deploy/auth.header \
-H "Content-Type: application/json" \
-d '{"data":{}}'
401
{"code":"render.issued-at-missing","detail":"The request was refused; use the problem code and the operator diagnostics to correct it.","status":400,"title":"issued-at-missing","traceId":"<trace-id>","type":"https://render.registrystack.org/problems/issued-at-missing"}

Render never supplies the issuance time for you. Use traceId to find the refusal in the service logs.

Terminal window
jq -c '{phase, outcome: .record.outcome, problem: .record.problem}' render-work/audit/render.jsonl
{"phase":"request","outcome":null,"problem":null}
{"phase":"response","outcome":"rendered","problem":null}
{"phase":"response","outcome":"refused","problem":"unauthorized"}
{"phase":"response","outcome":"refused","problem":"issued-at-missing"}

The render wrote a request entry before it started and a response entry with both hashes before the PDF left. Each refusal wrote one entry. Entries carry hashes, versions, and a fingerprint of the caller’s key, never the letter’s data.

When you are done, press Ctrl+C in the first terminal and delete render-work.