Versioned archive. You are viewing v0.38.0. For the latest released guidance, use Latest release. Report archive issues on GitHub.
You have a verified template package and callers that need PDFs, and you want registry-render serve
answering them. At the end of this page one registry-render process serves your package on a private
address behind your TLS proxy, answers GET /health and GET /ready, renders under API-key
authentication with a per-render audit trail, and shuts down within a bounded window.
Registry Render is pre-release software for institutional pilots. It has not joined a published release. The release roster admits its binaries and container image starting with v0.38.0. Until a release at or after that boundary is published, build the binary from source.
Rendering your first document covers the bundle side: scaffolding, authoring, and packaging. This page starts from a package directory.
What you provide
Section titled “What you provide”- One package directory, mounted read-only.
registry-render packagevalidates authoring source and writes a new directory with the sharedSHA256SUMSenvelope plus optionalREVISION. Serve verifies the envelope at startup and again in the worker on every request, then binds the exact captured files to the recorded digests before any validation or render. A package that drifts while the service runs is refused, not rendered. - One secret in an owner-only file: the caller API key. The key is at least 32 bytes of ASCII material; serve trims exactly one trailing line ending from the file and refuses to start if the key carries any other whitespace, because a silently mis-armed key would reject every caller while health stays green.
- One path for the audit file, or a log collector on standard output. The process appends one JSON line per audit entry and refuses a directory that group or others can write. One process writes one file, and a second process pointed at the same path refuses to start; give each replica its own path.
- A TLS proxy or ingress in front of the listener. The process serves plain HTTP, defaults to
127.0.0.1:8080, and refuses to start on a public or all-interfaces address. TLS termination, HSTS, and client addresses stay with your proxy, and the listener reads no forwarded header. - A host for one process. Renders run in supervised worker processes that are killed at the configured timeout and capped in address space on Linux, so one pathological template costs its timeout, not the service.
Install or build the binary
Section titled “Install or build the binary”For a published v0.38.0 or later release, choose <tag> from the
latest release, then follow the
release verification procedure at
https://github.com/registrystack/registry-stack/blob/<tag>/release/VERIFY.md before installing its
Linux amd64 asset. Set TAG to that tag. After verification:
tag="${TAG:?set TAG to a published tag that includes Registry Render}"mkdir -p ~/.local/bininstall -m 0755 "registry-render-${tag}-linux-amd64" ~/.local/bin/registry-renderexport PATH="$HOME/.local/bin:$PATH"registry-render --versionThis replaces any existing ~/.local/bin/registry-render. Preserve that file first when you need a
rollback path.
The same release publishes ghcr.io/registrystack/registry-render:<tag>. The image runs
/usr/local/bin/registry-render from Distroless cc-debian13 as the nonroot user. It starts
serve with /etc/registry-render/runtime.yaml, uses /var/lib/registry-render as its working
directory, and leaves the template package outside the image. Mount the runtime file and package
read-only. Keep the mounted API key owned by UID 65532 with mode 0400 or 0600, and the writable
audit directory owned by UID 65532 with mode 0700. Deploy the image by the digest authenticated
through release/VERIFY.md.
The loopback listener below needs a TLS proxy in the same network namespace. For a proxy in
another container, set listener.bind to Render’s private container address. Render refuses a
wildcard address such as 0.0.0.0.
From a Registry Stack checkout, build with the workspace lockfile, because the pinned compression stack is part of Render’s byte contract:
cargo build --release --locked -p registry-renderexport PATH="$PWD/target/release:$PATH"registry-render --versionThe version line names the release and the Typst pin, for example registry-render <version> (typst 0.15.1). Record it: every audit event carries the same string, and a stored PDF’s provenance is
this binary plus the package hash.
Write the runtime configuration
Section titled “Write the runtime configuration”Serve mode reads one YAML file, selected with --runtime-config. Every field is a contract, and
unknown fields are refused:
apiVersion: registry.registrystack.org/render-runtime/v1alpha1kind: RenderRuntimeConfiglistener: bind: 127.0.0.1:8080 shutdownGraceSeconds: 30package: # The directory written by `registry-render package`. root: /var/lib/registry-render/package # Optional: refuse to start on any other package digest. # expectedDigest: sha256:<64 lowercase hex digits>secretProviders: file: root: /run/secrets/registry-render # environment: {}auth: apiKeyRef: secret:file/render-api-keylimits: renderTimeoutSeconds: 20 maxOutputBytes: 8388608 maxRequestBodyBytes: 8388608 maxConcurrency: 4audit: path: /var/lib/registry-render/audit/render.jsonl rotateBytes: 67108864 retainDays: 90The runtime file path, package.root, audit.path, and secretProviders.file.root are
absolute, and the runtime file path must not pass through a symbolic link, so the same file behaves
identically wherever the process is launched from. listener.bind is required. A secret:file/name
reference resolves under secretProviders.file.root, and a secret:env/NAME reference only when
secretProviders.environment: {} is declared; a reference to an undeclared provider refuses
startup. String values may take a deployment value from the environment with ${VAR},
${VAR:-default}, or ${VAR:?message}, except in a *Ref field, which must be written as a
literal secret reference, and under secretProviders, which must be written as it is meant.
package.expectedDigest, when set, is compared with the package digest. A mismatch uses the
same expected/found message as every other Registry Stack runtime. GET /health reports the
same digest without the sha256: label as bundleHash for compatibility with Render’s response
and audit fields.
audit.destination is file by default; set it to stdout, without path, rotateBytes, or
retainDays, to hand the entries to your log collector instead.
The limits carry hard ceilings the runtime refuses to exceed: the output cap cannot exceed 8 MiB
and the request body cap 64 MiB, so a configuration mistake cannot unbound the process. On
shutdown the process drains in-flight renders for one grace period, gives open connections a
second one, and then abandons the rest; shutdownGraceSeconds is therefore the number to align
with your stop timeout, not a decorative field. It must be between 1 and 3600 seconds: zero would
drop the renders it exists to protect, and startup refuses both ends.
Start and verify
Section titled “Start and verify”Start the process with the runtime file, and check both endpoints before sending traffic:
registry-render serve --runtime-config /etc/registry-render/runtime.yaml &curl -s http://127.0.0.1:8080/healthcurl -s http://127.0.0.1:8080/readyGET /health answers with the bundle version and package hash and the renderer version, so you can
reconcile the running service against what you deployed. GET /ready includes the audit writer:
if it has stopped, readiness fails. Both are value-free.
registry-render healthcheck --runtime-config <file> performs the same probe for a supervisor script.
The rendering call
Section titled “The rendering call”Callers POST one JSON body to /v1/render/{type} with the API key and the issuance time:
curl -s -X POST http://127.0.0.1:8080/v1/render/<document-type> \ -H "Authorization: Bearer <api-key>" \ -H "Content-Type: application/json" \ -H "Accept: application/pdf" \ -o receipt.pdf \ -D headers.txt \ -d '{"issuedAt":"<issuance-time>","data":<document-data>}'grep -i x-registry headers.txtThe body’s data object must satisfy the document’s JSON Schema; violations come back as
problems with JSON pointers into the data, and registry-render validate dry-runs that check without
rendering.
The default response is the PDF bytes with X-Registry-Pdf-Sha256,
X-Registry-Data-Sha256, and X-Registry-Document-Version headers; Accept: application/json
returns the same facts plus the PDF as base64 for callers that cannot take binaries. Requests
must declare Content-Length: chunked transfer is refused, because a chunked body that exceeds
the limit mid-stream cannot be answered at all. Idempotency-Key is echoed and audited as an
opaque correlation id; rendering is deterministic, so no deduplication exists or is needed.
Failures are RFC 9457 problem documents with a closed vocabulary, on HTTP and in the CLI’s exit
codes. A render writes a request audit entry before the worker starts and a response entry
with the outcome before the document leaves; if either cannot be written, the call fails closed
with audit-failed and no document is returned. A refusal decided before any render, such as
invalid data or an unknown document type, is one response entry and fails closed the same way.
A 401 or an oversized body is also one response entry, but best-effort: a failed write is logged
at error level and the refusal still answers.
Read and ship the audit log
Section titled “Read and ship the audit log”Each line is one entry: schema (render.registrystack.org/audit/v1), eventId, time,
phase (request or response), correlation, and the value-free record: hashes, versions,
the caller’s key fingerprint, the outcome, and the trace and correlation ids, never data values or
image bytes. The request entry carries no outcome. Both entries of one render share their
correlation, a random identifier the server draws for every call; the caller’s Idempotency-Key
is recorded only as the record’s correlationId, because two calls may carry the same key. A call
that ends before its outcome is written, such as one whose caller disconnected, has the outcome
unfinished. For example, to list outcomes by correlation:
jq -c '{correlation, phase, outcome: .record.outcome}' /var/lib/registry-render/audit/render.jsonlWhen the active file would pass rotateBytes (100 MiB by default), it is renamed to
<path>.<sequence> with an eight-digit sequence and a fresh file opens. At startup and at each
rotation, sealed files last modified more than retainDays ago (90 by default) are deleted.
Never rename, edit, or truncate the active file while the process runs; the writer then stops and
every render fails closed until a restart.
The log carries no hash chain or signature, so it is not tamper-evident on the host. Ship sealed
files, or the stdout stream, to append-only storage before retention deletes them. A directory
that held the older chained ledger is not an audit path to reuse: archive its segments first and
point audit.path at a fresh directory.
registry-render check --bundle <dir> --runtime-config <file> --require-audit-under <root> proves the
audit file resolves under a persistent root, as a container preflight, and refuses a stdout
destination.
Upgrade discipline
Section titled “Upgrade discipline”A Render binary is exact source plus the Typst pin plus the lockfile, and any change to the Typst pin, the compression stack, or a template changes output bytes by design. Treat every upgrade as a golden-hash review: the two-OS job in CI fails on any byte difference and the diff is the review. Build every intentional change into a new output directory and switch the deployment to that immutable package. The package writer refuses an existing output directory, so a published digest never gains different bytes.