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 development stack it describes. Budget about ten minutes, most of which is the first container build.

Before you start

You need Docker Engine 24 or newer with Compose v2 (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 development stack runs PostgreSQL and MinIO in containers alongside Katagami.

1. Clone and start the stack

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

The first run compiles Katagami from source, so expect a few minutes. It starts three containers: the renderer on 127.0.0.1:8080, PostgreSQL, and MinIO for S3-compatible object storage. The development stack ships throwaway credentials and an admin token of smoke-admin-token.

Confirm the API is up:

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

A fresh installation answers:

{"templates":[]}

Note

Katagami has no health endpoint. This read request is how you check that a renderer started and loaded its catalog. If it does not answer, read the logs with docker compose -f docker-compose.dev.yml 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 persisted results are off unless you enable and request them.

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 a published version 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 -f docker-compose.dev.yml down --volumes

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

Troubleshooting

SymptomCauseFix
curl: (7) Failed to connect on 8080The renderer is still building or failed to startdocker compose -f docker-compose.dev.yml logs -f renderer
401 admin authentication requiredMissing or wrong x-admin-tokenThe development stack uses smoke-admin-token
409 on publishThat version is already published and cannot changePublish under a new version name
422 schema validation failedThe request data does not match the published JSON SchemaCompare against GET /v1/templates/{id}
PDF renders in the wrong fontTypst could not find that font family and used a fallbackUse the family name, not the file name
413The request or an object exceeded a limitSee Operations and limits
502 control plane unavailablePostgreSQL or object storage is unreachableCheck both containers and their credentials

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.