Skip to content
Registry StackDocsv0.38.0

Run Registry Render in serve mode

View as Markdown

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.

  • One package directory, mounted read-only. registry-render package validates authoring source and writes a new directory with the shared SHA256SUMS envelope plus optional REVISION. 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.

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:

Terminal window
tag="${TAG:?set TAG to a published tag that includes Registry Render}"
mkdir -p ~/.local/bin
install -m 0755 "registry-render-${tag}-linux-amd64" ~/.local/bin/registry-render
export PATH="$HOME/.local/bin:$PATH"
registry-render --version

This 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:

Terminal window
cargo build --release --locked -p registry-render
export PATH="$PWD/target/release:$PATH"
registry-render --version

The 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.

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/v1alpha1
kind: RenderRuntimeConfig
listener:
bind: 127.0.0.1:8080
shutdownGraceSeconds: 30
package:
# 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-key
limits:
renderTimeoutSeconds: 20
maxOutputBytes: 8388608
maxRequestBodyBytes: 8388608
maxConcurrency: 4
audit:
path: /var/lib/registry-render/audit/render.jsonl
rotateBytes: 67108864
retainDays: 90

The 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 the process with the runtime file, and check both endpoints before sending traffic:

Terminal window
registry-render serve --runtime-config /etc/registry-render/runtime.yaml &
curl -s http://127.0.0.1:8080/health
curl -s http://127.0.0.1:8080/ready

GET /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.

Callers POST one JSON body to /v1/render/{type} with the API key and the issuance time:

Terminal window
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.txt

The 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.

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:

Terminal window
jq -c '{correlation, phase, outcome: .record.outcome}' /var/lib/registry-render/audit/render.jsonl

When 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.

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.