Free, fast, schema-checked PDF rendering.

Publish a template with a JSON Schema. Every request is validated against it before a single glyph is typeset — then rendered in-process, with no headless browser and no per-page fees.

data.jsonPOST /render
{
  "customer": "Ada Lovelace",
  "total": 1200"1200",
  "due": "2026-02-01"
}
invoice.schema.jsonvalidate
customer · string
total · number
due · date
INVOICE
200 · 41ms
422
schema: /total expected number, got string
invalid data never reaches the renderer

How it works

From template to PDF in three steps.

PostgreSQL and S3 hold every published version. Renderers stay stateless, so any replica can restart, reload, and serve the same bytes.

01

Publish your Typst source, fonts, static assets, and a JSON Schema as one version. Objects are content-addressed, so a published version stays exactly as you shipped it.

02

Validate. Each request's data is checked against the version's schema before anything compiles. Invalid input fails early, with a clear error instead of a broken PDF.

03

Render. Typst runs in-process and returns PDF bytes — no CLI, subprocess, shell, or headless browser in the path.

Immutable

Versioned by content hash

Objects are content-addressed and checked against their manifest SHA-256. A published version renders identical bytes on every replica — indefinitely.

Lean path

No headless browser

Typst runs as a Rust library in the request path. No Chromium, no subprocess, no shell to spawn — that is where the throughput comes from.

Self-hosted

Your infrastructure, no per-page fees

Runs on the PostgreSQL and S3 you already operate. Free and open source, with templates and data that never leave your network.

Architecture

How the pieces fit together.

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.

Quickstart

Clone it and run the whole stack.

The development Compose file starts Katagami, PostgreSQL, and MinIO with local credentials — no cloud account needed. The production Compose file runs against object storage you already operate.

Publish a template and render a PDF
$ git clone https://github.com/BRO3886/katagami.git
$ cd katagami
$ docker compose -f docker-compose.dev.yml up --build -d
$ curl http://127.0.0.1:8080/v1/templates
{"templates":[]}

Benchmark

No browser in the path, so renders stay cheap.

See methodology
Steady-state invoice workloadThroughput
Katagami · pre-published Typst template2,151 req/s
Gotenberg · Chromium HTML conversion13.67 req/s

100 requests at concurrency 4, 2 CPU / 2 GiB per renderer on an 8-CPU ARM64 host, zero failures on both services. This compares a pre-published Typst template against browser-based HTML conversion for the same document — not a universal renderer claim. See the methodology for the full setup.