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:

Terminal
# .npmrc
@paperjsx:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${PAPERJSX_NPM_TOKEN}
Terminal
npm install @paperjsx/pptx-core

Engine Initialization

Create an engine instance with your license key:

TYPESCRIPT
import { CmdCal } from "@paperjsx/pptx-core";

const engine = new CmdCal({
  licenseKey: process.env.PAPERJSX_KEY!,
});

The constructor accepts a CmdCalOptions object:

OptionTypeDefaultDescription
licenseKeystring(required)License key from your dashboard
apiUrlstringhttps://api.paperjsx.comCustom 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.

TYPESCRIPT
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);
The self-hosted engine uses `PaperDocument` (the low-level AST), not `PresentationSpec` (the semantic V2 contract). If you are building from the hosted `PresentationSpec` format, the hosted API compiles it into a `PaperDocument` internally. For self-hosted use, you build the AST directly.

generate() -- Legacy AgentDocument

The generate() method accepts an AgentDocument (the V1 authoring format), compiles it into a PaperDocument, applies elastic pagination, and renders:

TYPESCRIPT
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:

TYPESCRIPT
const { pptx, previews } = await engine.generateWithPreviews(agentDoc);

for (let i = 0; i < previews.length; i++) {
  fs.writeFileSync(`slide-${i + 1}.png`, previews[i]);
}
Preview generation requires `@napi-rs/canvas` as an optional peer dependency. If it is not installed, `previews` will be an empty array and no error is thrown.

Font Loading

The engine auto-loads system fonts and bundles NotoSans as a fallback. To use custom fonts, load them before rendering:

TYPESCRIPT
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:

TYPESCRIPT
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=4096 for 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

DOCKERFILE
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"]
The native canvas dependencies (`libcairo2-dev`, etc.) are only required if you use `generateWithPreviews()` or `@napi-rs/canvas`. If you only need PPTX output, the `node:20-slim` base image is sufficient without the `apt-get` step.

Limitations vs. Hosted API

The self-hosted engine is a pure PPTX renderer. The following features are only available through the hosted API:

FeatureHosted APISelf-Hosted
PPTX renderingYesYes
PresentationSpec inputYesNo (PaperDocument only)
Brand packsYesNo
Preflight quality reportYesLimited (via PaperEngine.preflight)
Approval workflowYesNo
Baseline diffingYesNo
Desktop validationYesNo
Structural repairYesYes (via PaperEngine.validateAndRepair)
Job history and auditYesNo
Self-Hosting — CmdCal Docs