Documentation

Quickstart

Clone Katagami, publish a template, and render your first PDF on your own machine.

This walkthrough starts from an empty machine and ends with a PDF on disk. Every command below is copy-paste runnable and was run against the stack it describes. Budget about ten minutes, most of which is the first image pull.

Before you start

You need Docker Engine 24 or newer with Compose v2.24 or newer (docker compose, not docker-compose), plus git, curl, and jq. You do not need a Rust toolchain, a Typst install, or any cloud account. The quickstart stack runs PostgreSQL and MinIO in containers alongside the released Katagami image.

1. Clone and start the stack

git clone https://github.com/BRO3886/katagami.git
cd katagami
docker compose up -d

The default Compose file pulls the exact Katagami release image (currently ghcr.io/bro3886/katagami:0.0.2) instead of building from source, so expect a short pull on first run. It starts three containers: the renderer on 127.0.0.1:8080, PostgreSQL, and MinIO for S3-compatible object storage. The stack ships throwaway credentials and an admin token of smoke-admin-token.

Metrics are deliberately not published to your host. A Prometheus service on the Compose network can scrape http://renderer:9090/metrics; the public API returns 404 for /metrics.

Confirm the API is up:

curl http://127.0.0.1:8080/v1/templates

A fresh installation answers:

{"templates":[]}

Note

Use GET /health/ready to check whether a renderer can receive traffic. This catalog read does not preload the catalog; the first render of a version reads its stored pack through PostgreSQL and S3. If it does not answer, read the logs with docker compose logs -f renderer.

2. Write a template

A template is ordinary Typst source. Katagami passes the request body’s data object to your template as sys.inputs.at("data").

cat > invoice.typ <<'EOF'
#set page(width: 148mm, height: 105mm, margin: 12mm)
#set text(font: "Noto Sans", size: 11pt)

#text(size: 18pt, weight: "bold")[Invoice #sys.inputs.at("data").at("number")]

#v(6mm)
Billed to: #sys.inputs.at("data").at("customer")

Amount due: #sys.inputs.at("data").at("amount")
EOF

3. Build the publish payload

A published version carries its own schema, source, and fonts, so a render never depends on files that happen to sit on the host. The payload sends each file base64-encoded in a name-to-content map. source must contain main.typ, and fonts must contain at least one decodable font. The repository ships Noto Sans under assets/fonts/ for exactly this purpose.

jq -n \
  --arg source "$(base64 < invoice.typ | tr -d '\n')" \
  --arg font "$(base64 < assets/fonts/NotoSans-Regular.ttf | tr -d '\n')" \
  '{
    schema: {
      type: "object",
      additionalProperties: false,
      required: ["number", "customer", "amount"],
      properties: {
        number:   { type: "string" },
        customer: { type: "string" },
        amount:   { type: "string" }
      }
    },
    source: { "main.typ": $source },
    fonts:  { "NotoSans-Regular.ttf": $font }
  }' > invoice-v1.json

The schema field is a JSON Schema. Katagami validates every render request against it before Typst is invoked, so a caller that sends the wrong shape gets a clear 422 instead of a confusing compile error or a subtly wrong document.

Note

The map key is the font’s file name, but the name you use inside Typst is the font’s family name. NotoSans-Regular.ttf is written font: "Noto Sans" in the template. Mismatching the two is the most common first-publish failure.

4. Create the template and publish version one

Template IDs and versions are created separately: first the ID, then an immutable version under it.

curl --fail-with-body -X POST http://127.0.0.1:8080/v1/admin/templates \
  -H 'content-type: application/json' \
  -H 'x-admin-token: smoke-admin-token' \
  --data '{"id":"invoice"}'

curl --fail-with-body -X POST http://127.0.0.1:8080/v1/admin/templates/invoice/versions/v1 \
  -H 'content-type: application/json' \
  -H 'x-admin-token: smoke-admin-token' \
  --data-binary @invoice-v1.json

Both return 201 Created with an empty body. Both require the x-admin-token header; these two endpoints are the only way source ever enters the system.

5. Render a PDF

The render endpoint needs no admin token.

curl --fail-with-body -X POST http://127.0.0.1:8080/v1/templates/invoice/versions/v1/render \
  -H 'content-type: application/json' \
  --data '{"data":{"number":"INV-001","customer":"Ada Lovelace","amount":"1,240.00"}}' \
  --output invoice.pdf

Open it with open invoice.pdf on macOS or xdg-open invoice.pdf on Linux. The response is application/pdf bytes; nothing was written to object storage because this request did not ask for persistence (the Compose dev stack has ALLOW_PERSISTED_RESULTS=true, but a result is stored only when the request sends "output":{"persist":true}).

Watch the guardrails work

Three requests that should fail, and the exact answers you get. Running these is the fastest way to understand what Katagami enforces.

Data that does not match the published schema is rejected before compilation:

curl -X POST http://127.0.0.1:8080/v1/templates/invoice/versions/v1/render \
  -H 'content-type: application/json' \
  --data '{"data":{"number":"INV-001"}}'
{"error":"schema validation failed"}

Republishing a version that already exists returns 409, because the version content is immutable:

curl -o /dev/null -w '%{http_code}\n' \
  -X POST http://127.0.0.1:8080/v1/admin/templates/invoice/versions/v1 \
  -H 'content-type: application/json' \
  -H 'x-admin-token: smoke-admin-token' \
  --data-binary @invoice-v1.json
409

A publish without the right admin token returns 401:

{"error":"admin authentication required"}

Ship a change as a new version

Because versions never change in place, an update means publishing a new one. Callers already pinned to v1 keep getting exactly the document they were getting before.

sed 's/Invoice #/Invoice · v2 #/' invoice.typ > invoice-v2.typ
jq --arg source "$(base64 < invoice-v2.typ | tr -d '\n')" \
  '.source["main.typ"] = $source' invoice-v1.json > invoice-v2.json

curl --fail-with-body -X POST http://127.0.0.1:8080/v1/admin/templates/invoice/versions/v2 \
  -H 'content-type: application/json' \
  -H 'x-admin-token: smoke-admin-token' \
  --data-binary @invoice-v2.json

curl http://127.0.0.1:8080/v1/templates
{"templates":[{"id":"invoice","versions":["v1","v2"]}]}

Render both versions and compare them. v1 still produces its original document.

Stop the stack

docker compose down --volumes

Dropping --volumes keeps the PostgreSQL and MinIO data, so your published templates survive a restart.

Troubleshooting

Symptom Cause Fix
curl: (7) Failed to connect on 8080 The renderer is still pulling or failed to start docker compose logs -f renderer
401 admin authentication required Missing or wrong x-admin-token This quickstart uses the throwaway smoke-admin-token
409 on publish That version is already published and cannot change Publish under a new version name
410 on render The version is retired An administrator can unretire it if active capacity permits
422 schema validation failed The request data does not match the published JSON Schema Compare against GET /v1/templates/{id}
PDF renders in the wrong font Typst could not find that font family and used a fallback Use the family name, not the file name
413 Request data, an object, or a worker IPC frame exceeded a limit See Operations and limits
502 control plane unavailable PostgreSQL or object storage is unreachable Check both containers and their credentials
503 active template version capacity exceeded The active-version cap is full Retire an active version or raise the active limit
503 catalog retained-version byte capacity exceeded or incomplete Retained bytes are full or a legacy row has unknown bytes Raise the retained-byte limit or cold-render the legacy version; retirement does not help

Next steps

  • API reference for the full publish payload, request assets, and every status code.
  • Self-hosting to point Katagami at storage and a database you operate.
  • System architecture for what happens between the request and the PDF.