Component-first model
Templates are real React components compiled locally and executed in an isolated Chrome sandbox — never in Renduo's Node process.
Renduo's core model is component-first: a template is a real React component that consumes a JSON object as its props. You write the component, compile it locally, and register it once. Every generation after that is just a payload — Renduo executes your component in an isolated browser sandbox and returns the PDF.
This is the opposite of a template DSL. There is no proprietary block
vocabulary to learn, no {{ }} expression syntax, and no constraints on how
you lay out a document: if you can write it in React, it renders.
The register-once, generate-many flow
you (local) Renduo (cloud) Browserless (sandbox)
──────────────── ────────────────── ──────────────────────
write Component.tsx
esbuild → IIFE bundle ──────────────────────────►
renduo push (bundle + slug) validates statically
bakes self-contained HTML (React UMD inline,
shell into the version @font-face inline,
window.__RENDUO_PROPS__)
────────────────────────────────────────────────►
POST /v1/generate {templateId,
payload} resolves active version
injects payload into shell ─► fresh browser context
network blocked
render → page.pdf()
◄── base64 PDF
◄──────── 200 {pdf} (sync)
◄──────── 202 {jobId} (async)Registration (renduo push)
The CLI bundles your component locally with esbuild
(platform: browser, format: iife, react/react-dom as externals) and
uploads the resulting IIFE. Compiling on your side means you can use arbitrary
npm dependencies without us running third-party builds — and the only code we
ever execute is inside the sandbox.
esbuild picks its parser from your file's extension, so the extension you
choose has to match the syntax inside it — a component written with JSX needs
a .jsx/.tsx extension, not .js:
| Extension | JSX (<div>...</div>) | TypeScript types |
|---|---|---|
.js | ❌ not parsed — build fails | ❌ |
.jsx | ✅ | ❌ |
.ts | ❌ not parsed — build fails | ✅ |
.tsx | ✅ | ✅ |
.js only works if you skip JSX entirely and call React.createElement(...)
directly. In every other case — which is virtually always, for a real
component — use .jsx or .tsx.
The API validates the bundle without executing it (static checks only: IIFE
format, valid syntax, and that it reads props from window.__RENDUO_PROPS__),
then bakes a self-contained HTML shell: React and ReactDOM UMD inlined,
your bundle inlined, and a bootstrap that renders on load and signals
window.__RENDUO_READY__. The baked shell is versioned and stored once — the
baking cost is paid per version, never per PDF.
Generation
On each request, Renduo resolves the template's active version, injects the
serialized payload into the shell's placeholder, and loads it in a fresh
browser context with the network blocked (only about:/data:). It
waits for the render-ready signal, then captures page.pdf(). Sync mode
returns the binary as base64 directly; async mode enqueues a job, persists to
R2, and notifies you via webhook.
Why the sandbox
The original architecture (ADR-001) avoided running client code on the server for security reasons. The resolution: instead of not running it, run it inside the Chrome sandbox — the standard posture of the HTML-to-PDF industry. Your bundle never touches Renduo's Node process:
- A new browser context per render — no state can leak between tenants.
- Network blocked — the component cannot make outbound requests.
- Hard timeout — pathological templates are cut off.
- Bundle size limit at registration.
The only residual risk is a Chromium sandbox escape, which is the same risk the whole industry assumes; Browserless keeps Chrome patched.
Ephemeral sync mode
Sync generation is deliberately ephemeral: Renduo does not persist the payload, the PDF, or even a useful generation record. The PDF is returned once and Renduo holds nothing. This is a feature for clients with data-handling requirements (fintech), not a limitation.
What this means for you
- A template is code you already know how to write (React), not a DSL.
- Iteration happens locally with
renduo push; the API is stateless for sync. - Versioning is baked-in: pushing the same slug creates a new immutable version (see templates & versions).
See also
- Templates & versions — registration, immutability, and what "active" means.
- Sync vs async generation — the two modes and retention.
- CLI reference — the
renduocommands. - Quickstart — a working template end to end.