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, setDATABASE_URL, runpnpm db:migrate && pnpm db:seed, thenpnpm 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 2 —
thead { 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: avoidpagination 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 leafstyle="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>— safehttps://link rendered<br />before the phone number line — line break within a paragraph
Available templates
curl http://localhost:3000/templates
| Key | Description |
|---|---|
loan-approval-letter | Formal loan approval with terms summary, guarantor section, and fee schedule |
sample-invoice | Standard 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
| Document | Location |
|---|---|
| Full API reference | docs/api-guide.md |
| Template model / node types | docs/mvp-technical-spec.md |
| Visual editor guide | docs/editor-guide.md |
| Full request collection (VS Code REST Client) | docs/api-test.http |
| Release checklist | docs/release-checklist.md |