PDF Engine / Root Documentation
Sandboxed Templates Production-Facing Runtime Hash-Based Cache
Advanced Capable

Production Guide for the Entire App

This service is a filesystem-backed PDF rendering API built with FastAPI, sandboxed Jinja2, JSON Schema validation, WeasyPrint PDF generation, and request-hash caching. The root page documents the live system, not just the codebase.

6
Primary API routes
2
Health endpoints
API Key
X-API-Key: local-dev-key
Overview

System Overview

The app accepts structured render requests, validates them against a template-local JSON Schema, renders HTML with a sandboxed Jinja2 environment, converts that HTML to PDF with WeasyPrint, and stores the resulting bytes under a deterministic request hash.

Input Layer

FastAPI + Pydantic request validation.

Template Layer

Filesystem template bundles with metadata, schema, partials, and assets.

Render Layer

Sandboxed Jinja2 + WeasyPrint HTML-to-PDF conversion.

Cache Layer

Hash-addressed PDF storage in cache/.

Quickstart

Run Locally

python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
.venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000

Service URL: http://127.0.0.1:8000

OpenAPI docs: /docs

Root documentation: /

Default local auth header: X-API-Key: local-dev-key

Deployment

Docker Workflows

Production runtime

docker compose up api --build

The production image includes the native runtime dependencies required by WeasyPrint. The Compose service exposes port 8000 internally for Dokploy routing and persists runtime files in a named volume.

Architecture

Application Structure

  • app/routers/ HTTP routes and health/documentation entrypoints
  • app/services/template_service.py template bundle management and Jinja rendering
  • app/services/schema_service.py JSON Schema validation and payload limits
  • app/services/pdf_service.py WeasyPrint runtime integration
  • app/services/cache_service.py request hashing and PDF file persistence
  • app/services/render_service.py render orchestration and batch jobs
Templates

Template Contract

templates/{template_id}/
  template.html
  styles.css
  schema.json
  metadata.json
  partials/
  assets/

Supported template features:

  • Variables with {{ variable }}
  • Loops with {% for item in items %}
  • Conditions with {% if condition %}
  • Partial includes
Prompt

Prompt to Generate a Template from an HTML Design

Use this prompt when you want an LLM or design assistant to convert an existing HTML design file, HTML snippet, or static invoice page into a valid template bundle for this app.

You are converting an existing HTML design into a template bundle for a FastAPI PDF template engine.

Input:
- I will give you either:
  1. a full HTML design file, or
  2. an HTML snippet, or
  3. HTML + CSS code

Your task:
- Transform the design into a reusable template bundle for this system.
- Keep the visual structure and styling as close to the original design as possible.
- Replace hardcoded business content with Jinja2 template variables.
- Use loops for repeated items.
- Use conditions for optional sections.
- Split reusable header/footer sections into partials when appropriate.

Output format:
- Return these files:
  - template.html
  - styles.css
  - schema.json
  - metadata.json
  - optional partials/*.html if needed

Rules:
- This template will run inside a sandboxed Jinja2 environment.
- Do not use Python code, imports, custom runtime logic, or file access.
- Only use safe Jinja2 constructs:
  - {{ variable }}
  - {% for item in items %}
  - {% if condition %}
  - {% include "partials/file.html" %}
- Prefer dictionary-safe access patterns when helpful.
- Make the template generic and reusable, not tied to one company or one invoice.
- Keep all styling in styles.css where possible.
- Keep HTML semantic and clean.

Data modeling requirements:
- Infer a practical JSON payload structure from the design.
- Create a strict schema.json with:
  - type
  - required
  - properties
  - additionalProperties: false where appropriate
- Include arrays for repeated sections such as line items, table rows, benefits, features, or bullet blocks.
- Include optional fields for sections that may be hidden.

Metadata requirements:
- metadata.json must contain:
  - name
  - version
  - description

Template conversion expectations:
- Replace values like invoice number, name, dates, addresses, totals, descriptions, and branding with variables.
- Convert repeated cards/rows/list blocks into loops.
- Convert optional notes, subtitles, badges, terms, footers, and secondary panels into conditions.
- If the design has a repeating header or footer, move it into partials.

Return the final answer as:
1. Suggested template_id
2. Data contract summary
3. template.html
4. styles.css
5. schema.json
6. metadata.json
7. partial files if any

Example variable style:
- {{ data.invoice_number }}
- {{ data.customer.name }}
- {% for item in data["items"] %}
- {% if data.get("notes") %}

Wait for my HTML input and then generate the full template bundle.
API

API Surface

  • GET / advanced documentation index
  • GET /docs Swagger/OpenAPI UI
  • GET /health/live liveness probe
  • GET /health/ready readiness probe
  • GET /templates/{template_id} inspect a template bundle
  • POST /templates create a new template bundle
  • POST /render render one PDF or download URL response from template_id or template_object
  • POST /render/batch queue a batch render request
  • GET /jobs/{job_id} poll batch job status
  • GET /output/{file_id} retrieve generated output
Examples

Practical Request Examples

Inspect a template

curl http://127.0.0.1:8000/templates/invoice_v1

Render a PDF

curl -X POST http://127.0.0.1:8000/render   -H 'X-API-Key: local-dev-key'   -H 'Content-Type: application/json'   -d '{
    "template_id": "invoice_v1",
    "data": {
      "invoice_number": "INV-100",
      "issue_date": "2026-04-29",
      "customer": { "name": "Jane Doe", "email": "jane@example.com" },
      "items": [
        { "description": "Consulting", "quantity": 2, "unit_price": 100, "total": 200 }
      ],
      "notes": "Thank you"
    },
    "branding": { "company_name": "Acme" },
    "settings": { "filename": "invoice.pdf" }
  }' --output invoice.pdf

Return JSON with a download URL

curl -X POST http://127.0.0.1:8000/render   -H 'X-API-Key: local-dev-key'   -H 'Content-Type: application/json'   -d '{
    "template_id": "invoice_v1",
    "data": {
      "invoice_number": "INV-101",
      "issue_date": "2026-04-29",
      "customer": { "name": "Jane Doe", "email": "jane@example.com" },
      "items": [
        { "description": "Consulting", "quantity": 1, "unit_price": 150, "total": 150 }
      ]
    },
    "settings": { "filename": "invoice-url.pdf", "return_mode": "download_url" }
  }'

Render an inline template_object

curl -X POST http://127.0.0.1:8000/render   -H 'X-API-Key: local-dev-key'   -H 'Content-Type: application/json'   -d '{
    "template_object": {
      "template_html": "<html><body><h1>{{ data.title }}</h1></body></html>",
      "styles_css": "body { font-family: sans-serif; }",
      "schema": {
        "type": "object",
        "additionalProperties": false,
        "required": ["title"],
        "properties": {
          "title": { "type": "string" }
        }
      },
      "metadata": { "name": "Inline Template", "version": "1.0.0" }
    },
    "data": { "title": "Inline PDF" },
    "settings": { "filename": "inline.pdf", "return_mode": "download_url" }
  }'

Queue a batch

curl -X POST http://127.0.0.1:8000/render/batch   -H 'X-API-Key: local-dev-key'   -H 'Content-Type: application/json'   -d '{
    "template_id": "invoice_v1",
    "documents": [
      {
        "invoice_number": "INV-B1",
        "issue_date": "2026-04-29",
        "customer": { "name": "Jane Doe", "email": "jane@example.com" },
        "items": [
          { "description": "Consulting", "quantity": 1, "unit_price": 100, "total": 100 }
        ]
      }
    ]
  }'
Operations

Operational Guidance

Health Checks

curl http://127.0.0.1:8000/health/live
curl http://127.0.0.1:8000/health/ready

Auth Header

X-API-Key: local-dev-key

Tests

.venv/bin/python -m pytest

Cache Location

cache/{request_hash}.pdf

Readiness confirms that template and cache directories exist and that the PDF runtime is available. Liveness reports whether the process is up.

Security

Security Posture

  • Jinja2 uses SandboxedEnvironment
  • Template globals, filters, and tests are cleared
  • Payloads are schema-validated before render execution
  • Payload size and batch size are bounded in configuration
  • Templates remain decoupled from backend code paths

Still recommended for a fuller production rollout: authentication, audit logging, object storage, persistent job storage, and external queueing.

Caching

How Caching Works

The cache key is computed from:

  • template id
  • payload data
  • branding payload
  • render settings

Identical requests reuse the same cached PDF. Response headers include X-Request-Hash and X-Cache-Hit.

Artifacts

Generated Files

PDFs are written to the local cache directory:

cache/{request_hash}.pdf

They are also accessible through the mounted static route:

/cache/{request_hash}.pdf
Troubleshooting

Common Errors

  • Validation failures mean the input did not match schema.json
  • Template errors usually mean missing files or invalid Jinja expressions
  • Readiness failures often point to PDF runtime or filesystem issues

macOS native dependency note

brew install pango gdk-pixbuf libffi

This root page is the live operational guide for the running service. For filesystem-side repository docs, see README.md and index.md.