Documentation
System architecture
How Katagami keeps rendering stateless while template data stays durable.
Katagami splits into a durable control plane and a stateless data plane. PostgreSQL and S3-compatible storage hold everything that must survive; the render process holds nothing it cannot rebuild from them. That split is what lets you run replicas behind a load balancer without a shared working directory.
Ownership direction
Katagami keeps HTTP concerns, application decisions, domain types, and adapters in separate layers. Dependencies point one way only.
api parses HTTP requests and maps errors. application owns publication, registry
reload, validation, and rendering decisions. domain contains pure identifiers and
manifest validation. infrastructure connects PostgreSQL, S3-compatible storage, and
the Typst renderer.
The direction matters because the adapters implement port traits that application
defines. Swapping PostgreSQL or the object store is an infrastructure change, not a
rewrite of the publication rules.
Publication path
Publication is the only way source enters the system, and it is guarded end to end.
-
Accept the pack
An admin request carries a JSON Schema, the Typst source pack, font files, optional static files, and the allowlist of assets a render request may send.
schema · source · fonts · static assets
-
Write objects conditionally
Every object is stored under a key derived from its own content, created with an S3 precondition rather than a check-then-write. An existing object is never overwritten.
content-addressed key · If-None-Match: *
-
Verify every hash
Katagami loads the new version and checks each stored object against the SHA-256 recorded in its manifest, before that version is visible to any renderer.
each object vs manifest SHA-256
-
Reserve the version
PostgreSQL reserves shared catalog capacity inside a transaction held under an advisory lock, then records the immutable version. Publishers on separate replicas cannot exceed the shared limit.
PostgreSQL transaction · advisory lock
-
Reload the catalog
Each renderer reloads the published catalog from PostgreSQL and object storage every five seconds, so independently running replicas converge after a publication.
every replica · every 5 seconds
The result is durable storage with a stateless render process. A replica can restart, reload, and serve the same published versions without any shared render directory.
Render path
A render request never touches the host filesystem and never introduces new source.
-
Validate the data
The request body's data is checked against the JSON Schema published with that exact version. Invalid data is rejected before Typst is ever invoked.
published JSON Schema · 422 on failure
-
Check the assets
Named request assets are matched against the allowlist in the published manifest. A render request cannot introduce arbitrary Typst file names.
manifest allowlist · 400 on failure
-
Compile in-process
Typst runs inside the renderer process against its in-memory registry, under a concurrency cap and a hard deadline. There is no CLI, subprocess, or render directory.
4 concurrent · 10 second deadline
-
Return the PDF
The response is PDF bytes. A result reaches object storage only when ALLOW_PERSISTED_RESULTS is enabled and the request explicitly asks for it.
application/pdf · optional persist
Katagami does not use a Typst CLI, shell process, WASM runtime, host filesystem read, or render directory. Typst is linked in as a Rust crate and runs inside the request.
Why versions are immutable
A published version is a fixed unit: schema, source, fonts, static assets, and the hashes of every object it references. It cannot be edited or deleted. Changing a template means publishing a new version.
That constraint pays for itself in three places. A caller pinned to v1 cannot be
broken by someone else’s template edit. A rendered document can be reproduced later
from the version it names. And because objects are content-addressed and written with
a precondition rather than a check-then-write, two publishers racing on separate
replicas cannot corrupt each other’s version.
The cost is that mistakes are shipped forward rather than patched in place. Publish
v2; there is no way to fix v1.