A few months ago I found myself staring at a 400-line Handlebars template for an invoice, trying to keep it in sync with the same invoice we render in React. Every time the invoice design changed, I had to update both implementations and hope they still produced the same document.
I realized I was maintaining the same logic twice. The React component already knew how an invoice looked. I was the one adding a second source of truth.
So I built Renduo — an API that generates PDFs from your existing React components. Push once, generate from any backend. No template language. No browser infrastructure to manage. No sync drift.
The State of PDF Generation in 2026
Most PDF generation approaches I've worked with fall into one of three buckets, and each has a structural problem.
Approach 1: Puppeteer / Playwright DIY
You spin up a headless browser, point it at an HTML page, and print to PDF. It works — until it doesn't.
The problems compound fast: managing browser concurrency, handling timeouts, dealing with Chromium memory leaks on long-running processes, queueing jobs so you don't melt your server. What starts as a 20-line script becomes a second product you maintain alongside your actual product.
And you're still writing HTML/CSS by hand. The browser is just the renderer — you haven't solved the template problem.
Approach 2: Template-First APIs
Services like CraftMyPDF, PDFMonkey, and APITemplate give you a PDF API, but they expect you to build your documents in their language — Jinja, Liquid, or a proprietary drag-and-drop editor.
This means you now maintain two representations of every document: your React component for the UI, and the service's template for the PDF. When the design changes — and it always does — you update both. You will forget one. The sync drift is inevitable, and it compounds with every template.
Approach 3: Client-Side Libraries
jsPDF, pdf-lib, and similar are powerful, but they're imperative. You position elements by coordinates. You build tables row by row. You can't reuse the React component you already have — you're redrawing the entire document from scratch in a different paradigm.
And they run in the browser or Node, so you're still managing the infrastructure: where does this run? At request time? In a queue? How do you handle a 50-page document that takes 8 seconds to lay out?
None of these approaches reuse what you already have: the React component.
The Component-First Model
Here's the core idea: your React component is the template.
Here's what happens step by step:
Step 1 — Push. You have a React component. You run renduo push Invoice.tsx from the CLI. It compiles your component with esbuild locally — react and react-dom are marked as externals because they'll be provided by the shell. The result is an IIFE bundle.
Step 2 — Bake. The bundle gets baked into a self-contained HTML shell (React UMD + ReactDOM UMD + your bundle, all inlined). This shell is uploaded to R2 and registered as an immutable version. Push the same slug again, and it creates version 2 — version 1 stays intact and can be regenerated from the exact same template version.
Step 3 — Generate. Your backend sends a POST with a templateId and a payload — just the props your component expects. Renduo loads the shell, injects your payload, and executes it inside an isolated Chrome context (Browserless). The network is blocked — the only allowed protocols are about: and data:. The Chrome context is created fresh per generation and destroyed immediately after.
Your code never executes in Renduo's Node process. Not once. Not partially. Never. The entire execution happens inside a sandboxed browser context that can't reach the network, can't touch the filesystem, and can't affect other generations.
Zero to First PDF
Let's go from nothing to a real invoice PDF.
1. Your component — exactly the one in your codebase
2. Push it once
3. Generate from anywhere
4. Or use the SDK
And here's what comes out:
That's it. You wrote a React component — the same one that renders in your app. You pushed it once. Now any backend can generate PDFs from it with a single HTTP call.
Architecture Decisions Worth Stealing
This section isn't a tutorial. It's the behind-the-scenes reasoning that might save you time if you're building something similar.
Why the Component Never Executes in Node
Most PDF APIs receive your template and execute it inside their API process. You send them HTML with Liquid tags, they eval() or parse it server-side.
Renduo doesn't. Here's why:
This isn't innovative. It's the same isolation model that every browser already provides between websites. If your component has a
while(true), it freezes one Chrome tab — not my API. If it triesfetch("https://evil.com"), the request is blocked at the browser level because the Chrome context allows onlyabout:anddata:protocols. If it consumes too much memory, the isolated browser context can be terminated without taking down the API process. None of this is "we audit your code" — it's "your code can't reach anything, by design."Multi-tenancy without multi-tenant anxiety. Every generation gets a brand-new Chrome context. There is no shared state between generations, no cached module scope, no cross-contamination. Two customers generating simultaneously are two isolated tabs.
Why Versions Are Immutable
Every renduo push with the same slug creates a new version and disables the previous one. There is no "edit in place," no draft that overwrites production.
The trade-off: if you accidentally push a broken template, you can't "revert" from the dashboard. You push again with the correct code — same slug, same --name. Version 3 becomes active, version 2 is archived.
This is deliberate. Immutable versions mean:
- Regeneration is reproducible. The same template version and payload are rendered from the same immutable template, without relying on whatever version happens to be deployed today. An invoice from March 2026 regenerated in 2028 uses the exact same template version that produced it.
- No deployment anxiety. Pushing version 3 doesn't delete version 2. If you need the old PDF for an audit, it's regeneratable exactly as it was.
- Auditability without effort. Every generation records which template version produced it. There's no mystery about why two PDFs look different — they used different versions.
The missing "rollback" UI isn't an oversight — it's avoiding a button that says "go back to an old version" when the correct action is "push the correct code." Explicit beats implicit.
Why Sync Mode Is Ephemeral by Design
When you generate in mode: "sync", Renduo returns the PDF as base64 in the response body. The PDF is never written to disk. The payload is never stored. Only metadata — generationId, templateId, durationMs, status — is persisted.
This isn't a missing feature — it's a data-retention guarantee.
If your use case involves sensitive data (medical records, legal documents, financial statements), you want a clear answer to "does the provider store this?" With sync mode, the answer is provably "no."
The trade-off: if you need the PDF later, you store it. If you need retry logic, you implement it on your side. This is the deliberate cost of an ephemeral guarantee — and it's why async mode exists for the opposite use case (store the PDF, notify via webhook, retain per your plan).
The 100-Page Cap
Every plan, every mode, every customer: maximum 100 pages per document.
This isn't a pricing limitation. It's a guardrail. A 500-page document would take minutes to render and degrade latency for every other generation in the queue. The cap ensures that one pathological request doesn't become everyone else's problem.
If you genuinely need >100 pages, talk to us about Enterprise. We'll figure it out. But the default is a cap — and caps are communicated, not just enforced.
What We Got Wrong (So Far)
I promised honesty. Here are two things that didn't go as planned.
The Latency Lie
Early in development, I ran a benchmark on a synthetic component — an empty div with no styles, no fonts, no assets. Measured from a dedicated harness co-located with Browserless. Result: 278ms.
I was thrilled. "Sub-300ms PDF generation!" I wrote in my notes.
Then I measured with a real template — a component with Geist Sans (a web font, loaded as a custom asset), tables, styling, realistic data. Measured from production topology (Railway → Browserless). Result: p95 of 900–1000ms for single-page documents.
The 278ms was a lie. Not an intentional one — a self-deception born from measuring in ideal conditions that don't represent reality. The lesson: laptop benchmarks and synthetic components are meaningless. If you can't measure from your production topology with a real workload, don't publish numbers at all.
I'm publishing the 900–1000ms figure because it's honest. It's not sub-100ms. It's not sub-50ms. It's "under a second for a typical document," which is the real performance envelope of this architecture.
We Have No Customers Yet
This product launches as you're reading this. There are no logos, no case studies, no testimonials. The landing page literally says "we're pre-launch — no logos yet."
If that bothers you, I get it. Social proof is real. But I'd rather be honest about zero than pretend a friend's startup "uses Renduo in production with great results." You deserve to evaluate the product, not the marketing.
The free plan gives you 100 PDFs/month without a credit card. Try it. Break it. Tell me what's wrong. That's the only validation that matters.
Try It
- Free: 100 PDFs/month, 5 templates, sync + async, no card
- Docs: docs.renduo.dev
-
SDK:
npm install @renduo/sdk -
CLI:
npm install -g @renduo/cli
I'm @byjohans_dev on X if you want to tell me what I got wrong.

Top comments (0)