Pulp Engine Document Rendering
Get started

Pulp Engine — Demo Guide

Time to first PDF: ~5 minutes (assumes Node 22 and pnpm installed; no database required)

Pulp Engine is a schema-driven document rendering service. You POST a template key and JSON data; it returns a PDF. Any language stack — C#, Python, JavaScript, PHP, VB.NET — calls it over HTTP. Templates are defined as structured JSON; no code changes are needed to add or update them.


Setup

The fastest path uses file mode — no PostgreSQL or other database required. Templates are loaded from the ./templates directory, which ships with ready-to-run examples.

# 1. Install dependencies
pnpm install

# 2. Configure environment (file mode — no database needed)
cp .env.example .env
# In .env: set STORAGE_MODE=file and TEMPLATES_DIR=./templates
# Also set API_KEY_ADMIN to any non-empty string for local dev
# (or leave all keys unset — auth is disabled in development with a warning)

# 3. Generate Prisma client (compiles types only; no DB connection made)
pnpm db:generate

# 4. Build all packages
pnpm build

# 5. Start the API
pnpm --filter @pulp-engine/api dev
# → Pulp Engine API running on http://0.0.0.0:3000

Confirm it’s up:

curl http://localhost:3000/health
# { "status": "ok", "timestamp": "..." }

PostgreSQL setup (optional): If you prefer to evaluate against a real database, set STORAGE_MODE=postgres, set DATABASE_URL, run pnpm db:migrate && pnpm db:seed, then pnpm build. The API and demo steps below are identical in both modes. See deployment-guide.md for full database setup.


Demo Walkthrough

Work through these five steps in order. Each one shows a different system behaviour.


Step 1 — Happy path: render a PDF

curl -s -X POST http://localhost:3000/render \
  -H "Content-Type: application/json" \
  -d '{
    "template": "loan-approval-letter",
    "data": {
      "applicantName":     "Jane Smith",
      "applicantAddress":  "12 Maple Street, Sydney NSW 2000",
      "loanAmount":        50000,
      "interestRate":      5.75,
      "termMonths":        60,
      "settlementDate":    "2026-04-01",
      "requiresGuarantor": true,
      "guarantorName":     "Bob Smith",
      "items": [
        { "description": "Application fee", "amount": 250 },
        { "description": "Valuation fee",   "amount": 400 },
        { "description": "Legal fee",       "amount": 800 }
      ]
    }
  }' --output demo-happy.pdf

start demo-happy.pdf   # Windows
# open demo-happy.pdf  # macOS

What to look for:

  • Recipient name “Jane Smith” mapped from applicantName — field mapping at work
  • Loan amount shows as A$50,000.00 — currency helper with explicit AUD code
  • Settlement date shows as 1 April 2026 — date helper with long format, en-AU locale
  • Guarantor section is present (highlighted amber box) — conditional node evaluated to true
  • Fee table has three rows with right-aligned A$ amounts and alternating row shading
  • Table header (“Description” / “Amount”) repeats on every printed page
  • Opening letter body uses a richText node: “Dear Jane Smith,” (Handlebars), “conditionally approved” in bold, “Important:” in burnt-orange, a three-item action list with a nested sublist, and a link to the full terms

Step 2 — HTML preview (debug without Puppeteer)

Same payload, different endpoint. Returns the HTML string the PDF renderer would receive.

curl -s -X POST http://localhost:3000/render/html \
  -H "Content-Type: application/json" \
  -d '{
    "template": "loan-approval-letter",
    "data": {
      "applicantName": "Jane Smith",
      "loanAmount": 50000,
      "interestRate": 5.75,
      "termMonths": 60,
      "requiresGuarantor": false,
      "items": []
    }
  }' > demo-preview.html

start demo-preview.html

What to look for:

  • Guarantor section is absent — conditional node evaluated to false, no empty placeholder
  • Fee table body shows No items. spanning both columns — empty-state message
  • View source: <thead> inside @media print { thead { display: table-header-group } } — print repeat header
  • Faster than PDF (~100 ms vs ~3 s) — useful during template development

Step 3 — Validation failure (422)

Data is field-mapped and then validated against the template’s JSON Schema. Missing required fields return a structured error before any rendering occurs.

curl -s -X POST http://localhost:3000/render \
  -H "Content-Type: application/json" \
  -d '{ "template": "loan-approval-letter", "data": {} }'

Expected response — 422:

{
  "error": "Validation Failed",
  "issues": [
    { "path": "/customer/name", "message": "must be string" },
    { "path": "/loan/amount",   "message": "must be number" },
    ...
  ]
}

What to observe: Paths reference the adapted shape (customer.name, loan.amount) — not the raw payload keys. The field mapping ran first; schema validation ran second.


Step 4 — Unknown template (404)

curl -s -X POST http://localhost:3000/render \
  -H "Content-Type: application/json" \
  -d '{ "template": "does-not-exist", "data": {} }'

Expected response — 404:

{
  "error": "Not Found",
  "message": "Template \"does-not-exist\" not found."
}

No stack trace. Error handler maps domain errors to clean HTTP responses.


Step 5 — Pagination (table spanning multiple pages)

curl -s -X POST http://localhost:3000/render \
  -H "Content-Type: application/json" \
  -d '{
    "template": "loan-approval-letter",
    "data": {
      "applicantName": "Pagination Test",
      "loanAmount": 100000,
      "interestRate": 4.5,
      "termMonths": 120,
      "requiresGuarantor": false,
      "items": [
        { "description": "Fee 01", "amount": 100 },
        { "description": "Fee 02", "amount": 100 },
        { "description": "Fee 03", "amount": 100 },
        { "description": "Fee 04", "amount": 100 },
        { "description": "Fee 05", "amount": 100 },
        { "description": "Fee 06", "amount": 100 },
        { "description": "Fee 07", "amount": 100 },
        { "description": "Fee 08", "amount": 100 },
        { "description": "Fee 09", "amount": 100 },
        { "description": "Fee 10", "amount": 100 },
        { "description": "Fee 11", "amount": 100 },
        { "description": "Fee 12", "amount": 100 },
        { "description": "Fee 13", "amount": 100 },
        { "description": "Fee 14", "amount": 100 },
        { "description": "Fee 15", "amount": 100 },
        { "description": "Fee 16", "amount": 100 },
        { "description": "Fee 17", "amount": 100 },
        { "description": "Fee 18", "amount": 100 },
        { "description": "Fee 19", "amount": 100 },
        { "description": "Fee 20", "amount": 100 }
      ]
    }
  }' --output demo-pagination.pdf

start demo-pagination.pdf

What to look for:

  • PDF has 2 or more pages
  • Table header row (“Description” / “Amount”) repeats at the top of page 2thead { display: table-header-group } CSS rule
  • No row is split across a page boundary — each fee row appears complete on one page
  • Signature block stays together on its own page — breakInside: avoid pagination hint

Step 6 — richText: formatted body content

The loan-approval-letter body uses a richText node for the opening paragraphs. Re-use the Step 2 HTML payload to inspect the rendered markup directly:

curl -s -X POST http://localhost:3000/render/html \
  -H "Content-Type: application/json" \
  -d '{
    "template": "loan-approval-letter",
    "data": {
      "applicantName": "Jane Smith",
      "loanAmount": 50000,
      "interestRate": 5.75,
      "termMonths": 60,
      "requiresGuarantor": false,
      "items": []
    }
  }' > demo-richtext.html

# Then open demo-richtext.html in your browser

What to look for in the HTML source:

  • Dear Jane Smith, — Handlebars resolved inside a <p> from a text leaf
  • <strong>conditionally approved</strong> — bold mark on a single text leaf
  • style="color:#c05000" on the “Important:” span — hex colour applied to a text leaf
  • <ul> with three <li> items; the second <li> contains a nested <ul> for “Check the repayment schedule on page 4” — one level deep, which is the maximum nesting depth the richText AST supports
  • <a href="https://example.com/terms">Read the full loan terms and conditions</a> — safe https:// link rendered
  • <br /> before the phone number line — line break within a paragraph

Available templates

curl http://localhost:3000/templates
KeyDescription
loan-approval-letterFormal loan approval with terms summary, guarantor section, and fee schedule
sample-invoiceStandard invoice with line items, GST, and total

To see the input schema or get a sample payload for any template:

curl http://localhost:3000/templates/loan-approval-letter/schema
curl http://localhost:3000/templates/loan-approval-letter/sample

Further reading

DocumentLocation
Full API referencedocs/api-guide.md
Template model / node typesdocs/mvp-technical-spec.md
Visual editor guidedocs/editor-guide.md
Full request collection (VS Code REST Client)docs/api-test.http
Release checklistdocs/release-checklist.md