Registry stack documentation: machine-readable Markdown.
Index of all pages: https://docs.registrystack.org/v/0.38.0/llms.txt
Full corpus: https://docs.registrystack.org/v/0.38.0/llms-full.txt

# Run Registry Render in serve mode

> Install or build Registry Render, write its runtime configuration, provision its API key, start serve mode behind your TLS proxy, and read and ship its audit log.

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.

{/* Evidence: products/render/ACCEPTANCE.md;
    products/render/DEFINITION-OF-DONE.md;
    release/scripts/release_roster.py, RENDER_FIRST_RELEASE and render_in_release. */}

[Rendering your first document](../../tutorials/first-render-document/) covers the bundle side:
scaffolding, authoring, and packaging. This page starts from a package directory.

## What you provide

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

{/* Evidence: crates/registry-render/src/runtime.rs, RenderRuntime and validate_bind;
    crates/registry-render/src/server.rs, normalize_api_key;
    crates/registry-render/src/worker.rs, supervise and cap_address_space;
    crates/registry-render/src/audit.rs, RenderAudit;
    crates/registry-render/src/bundle.rs, load_package and bind_verified_snapshot. */}

## Install or build the binary

For a published v0.38.0 or later release, choose `<tag>` from the
[latest release](https://github.com/registrystack/registry-stack/releases/latest), 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:

```sh
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`.

{/* Evidence: release/scripts/build-release-binaries.sh, registry-render asset staging;
    release/docker/Dockerfile.registry-render;
    crates/registry-render/src/runtime.rs, validate_bind. */}

From a Registry Stack checkout, build with the workspace lockfile, because the pinned compression
stack is part of Render's byte contract:

```sh
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.

{/* Evidence: crates/registry-render/src/lib.rs, display_version and TYPST_PIN. */}

## Write the runtime configuration

Serve mode reads one YAML file, selected with `--runtime-config`. Every field is a contract, and
unknown fields are refused:

```yaml
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.

{/* Evidence: crates/registry-render/src/runtime.rs, LimitsRuntime, AuditRuntime, and validate_bind;
    crates/registry-render/src/server.rs, serve. */}

## Start and verify

Start the process with the runtime file, and check both endpoints before sending traffic:

```sh
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.

{/* Evidence: crates/registry-render/src/server.rs, health and healthcheck;
    crates/registry-render/src/openapi.rs, OPENAPI_JSON. */}

## The rendering call

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

```sh
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.

{/* Evidence: crates/registry-render/src/server.rs, render_route and refuse_oversized_bodies;
    crates/registry-render/src/problem.rs, ProblemKind;
    crates/registry-render/src/audit.rs, append;
    crates/registry-render/src/openapi.rs, OPENAPI_JSON. */}

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

```sh
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.

{/* Evidence: crates/registry-render/src/audit.rs, AUDIT_SCHEMA, RenderAuditEvent, and correlation;
    crates/registry-render/src/audit.rs, RenderAudit::request writes unfinished when dropped;
    crates/registry-render/src/server.rs, the_request_entry_is_accepted_before_the_render_starts;
    crates/registry-render/src/cli.rs, require_audit_under;
    crates/registry-platform-audit/src/writer.rs, DEFAULT_AUDIT_ROTATE_BYTES and DEFAULT_AUDIT_RETAIN_DAYS. */}

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

{/* Evidence: .github/workflows/render-golden.yml;
    products/render/EVIDENCE.md. */}

## Next

- [Render your first document](../../tutorials/first-render-document/)
- [How Registry Render stays byte-stable](../../explanation/render-determinism/)
- [Registry Render overview](../../start/registry-render/)