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