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.

Client api/ HTTP DTOs · status application/ use cases · port contracts domain/ pure template types PostgreSQL adapter versions · manifests S3 object store source · fonts · assets Typst renderer in-process crate implements ports lib.rs composes the adapters into application/ at startup; config/ reads AppConfig once.
Dependencies point right: api → application → domain. Infrastructure adapters implement application ports.

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.

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

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

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

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

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

A version is hash-verified before it enters the published repository, so it can never become visible with missing or tampered objects.

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.

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

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

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

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

Nothing is stored by default. Without persistence enabled and requested, no render JSON or PDF is written anywhere.

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.