Documentation

System architecture

How Katagami keeps rendering stateless while template data stays durable.

Katagami splits into a durable catalog and a stateless render path. PostgreSQL and S3-compatible storage hold everything that must survive; each server holds only a read-through cache and a supervised Typst worker pool. That split is what lets you run replicas behind a load balancer without a shared working directory.

Runtime topology

Client Server API HTTP · schema Version gate keyed · generation Registry read-through · LRU Worker pool jobs · deadline Typst worker 1 Typst worker 2 Typst worker N Catalog LISTEN/NOTIFY · periodic reconciliation PostgreSQL S3 · objects/{sha256}

The API accepts publish and render requests. The registry is an in-memory LRU of hot published versions — never a full mirror of the catalog. A miss read-throughs PostgreSQL and S3, verifies every object against its manifest SHA-256, then inserts. A process-local version coordinator serializes publish, load, retire, and unretire work for the same version. Different version keys do not share an I/O lock, so slow storage or catalog work for one version does not stop warm reads of another. The worker supervisor maintains a fixed pool of child processes. Each worker handles one compile; a deadline or worker failure kills and reaps that child, then replacement is bounded by the restart budget.

PostgreSQL is the catalog and shared-capacity authority. S3 holds content-addressed objects under objects/{sha256}. The keyed coordinator is local to one replica. Cross-replica lifecycle notifications, reconnect handling, periodic reconciliation, and bounded staleness keep replica registries convergent without eager object loads.

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.

    objects/{sha256} · 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. Warm the local registry

    After PostgreSQL commits, the publishing replica inserts the verified version into its local cache. PostgreSQL LISTEN/NOTIFY invalidates peers, and freshness-bounded reconciliation repairs missed notifications under ADR 0004.

    per-version gate · local LRU

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

The result is durable storage with a rebuildable render process. A replica can restart, miss-fetch a version on first use, and serve the same published bytes without any shared render directory.

Render path

A render request never touches the host filesystem and never introduces new source.

  1. Resolve the version

    One replica coordinates each version independently. A miss loads through PostgreSQL and S3, verifies each SHA-256, and inserts under the cache budget. LISTEN/NOTIFY and reconciliation keep replica registries fresh.

    keyed gate · registry hit or verified read-through

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

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

  4. Compile in a worker

    The supervisor dispatches each compile to one worker process. On a deadline or worker death it kills and reaps that child; the restart budget controls replacement capacity.

    supervised child · deadline · IPC

  5. 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 depend on a host Typst CLI, arbitrary shell process, WASM runtime, host filesystem read, or render working directory. It runs only its own supervised worker binary, which provides hard deadline and crash isolation.

Why versions are immutable

A published version is a fixed unit: schema, source, fonts, static assets, and the hashes of every object it references. Its content cannot change. Changing a template means publishing a new version. A version may later be retired — it stays durable for audit and reproducibility, drops out of the registry and active-version quota, still counts against MAX_STORAGE_BYTES, and renders answer 410 Gone.

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.