Self-Hosting
The @paperjsx/pptx-core package lets you run the CmdCal rendering engine inside your own infrastructure. It produces the same PPTX output as the hosted API, but you control where the process runs.
When to Self-Host
Self-hosting is the right fit when:
- Your security policy requires all document generation to happen inside your own network boundary.
- You need to render in airgapped or offline environments (the license has a 72-hour offline grace period).
- You want direct control over concurrency, memory, and scaling.
Use the hosted API when you need brand packs, approval workflows, baseline diffing, quality policy enforcement, or the preflight pipeline. These features are not available in the self-hosted engine.
Installation
See SDK Installation for registry setup. In short:
# .npmrc
@paperjsx:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${PAPERJSX_NPM_TOKEN}
npm install @paperjsx/pptx-core
Engine Initialization
Create an engine instance with your license key:
import { CmdCal } from "@paperjsx/pptx-core";
const engine = new CmdCal({
licenseKey: process.env.PAPERJSX_KEY!,
});
The constructor accepts a CmdCalOptions object:
| Option | Type | Default | Description |
|---|---|---|---|
licenseKey | string | (required) | License key from your dashboard |
apiUrl | string | https://api.paperjsx.com | Custom license validation URL |
License validation runs automatically on first use. After successful validation, the license is cached for 24 hours. If the validation server is unreachable, a 72-hour grace period allows offline use. No presentation data is transmitted during validation.
renderDocument()
The primary method for self-hosted rendering. Takes a PaperDocument AST and returns a PPTX buffer.
import type { PaperDocument } from "@paperjsx/pptx-core";
const doc: PaperDocument = {
type: "Document",
meta: { title: "Quarterly Review" },
slides: [
{
type: "Slide",
children: [
{
type: "Text",
content: "Revenue Summary",
style: { x: 80, y: 60, width: 700, fontSize: 32, fontWeight: "bold" },
},
{
type: "Text",
content: "ARR reached $12.4M in Q3, up 24% year-over-year.",
style: { x: 80, y: 120, width: 700, fontSize: 18 },
},
],
},
],
};
const pptx: Buffer = await engine.renderDocument(doc);
generate() -- Legacy AgentDocument
The generate() method accepts an AgentDocument (the V1 authoring format), compiles it into a PaperDocument, applies elastic pagination, and renders:
const pptx = await engine.generate({
title: "Board Update",
slides: [/* AgentDocument slides */],
});
This method exists for backward compatibility. New integrations should use renderDocument() with a PaperDocument.
Preview Generation
The generateWithPreviews() method renders the PPTX and attempts to produce PNG slide previews:
const { pptx, previews } = await engine.generateWithPreviews(agentDoc);
for (let i = 0; i < previews.length; i++) {
fs.writeFileSync(`slide-${i + 1}.png`, previews[i]);
}
Font Loading
The engine auto-loads system fonts and bundles NotoSans as a fallback. To use custom fonts, load them before rendering:
import { loadFont } from "@paperjsx/pptx-core";
import { readFileSync } from "node:fs";
const buffer = readFileSync("./fonts/BrandSans-Regular.ttf");
await loadFont("Brand Sans", buffer);
For full HarfBuzz text shaping (required for complex scripts), use loadFontWithHarfBuzz instead:
import { loadFontWithHarfBuzz } from "@paperjsx/pptx-core";
await loadFontWithHarfBuzz("Brand Sans", buffer);
Memory and Performance
The engine uses a render mutex internally -- only one render runs at a time per process. Key considerations:
- WASM initialization: The first render in a process loads Yoga (layout) and HarfBuzz (text shaping) WASM modules. Expect a cold-start overhead of 200-500ms.
- Memory: A 20-slide deck with charts typically uses 100-200 MB peak. Set
NODE_OPTIONS=--max-old-space-size=4096for larger decks. - Concurrency: To render in parallel, run multiple Node.js processes or worker threads. Each process has its own render mutex.
- Font cache: Fonts are cached in-process. Call
clearFontCache()if you need to reclaim memory after many font families have been loaded.
Docker Deployment
FROM node:20-slim
RUN apt-get update && apt-get install -y \
libcairo2-dev libjpeg-dev libpango1.0-dev libgif-dev librsvg2-dev \
fontconfig fonts-noto-core \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY package.json .npmrc ./
RUN npm ci --production
COPY . .
ENV PAPERJSX_KEY=pj_live_your_key_here
ENV NODE_OPTIONS=--max-old-space-size=4096
CMD ["node", "server.js"]
Limitations vs. Hosted API
The self-hosted engine is a pure PPTX renderer. The following features are only available through the hosted API:
| Feature | Hosted API | Self-Hosted |
|---|---|---|
| PPTX rendering | Yes | Yes |
| PresentationSpec input | Yes | No (PaperDocument only) |
| Brand packs | Yes | No |
| Preflight quality report | Yes | Limited (via PaperEngine.preflight) |
| Approval workflow | Yes | No |
| Baseline diffing | Yes | No |
| Desktop validation | Yes | No |
| Structural repair | Yes | Yes (via PaperEngine.validateAndRepair) |
| Job history and audit | Yes | No |