Pulp Engine Document Rendering
Get started
Release v0.73.0

Release v0.73.0 — Playground, streaming SDKs, credibility pass

Date: 2026-04-19 Tag: v0.73.0

Summary

v0.73.0 ships the public playground at /playground, completes the streaming SDK surface for Go and .NET, and a full credibility pass on the landing page — honest copy, working links, consistent framing, and accurate counts. The Evaluation Licence moves to New Zealand law.

No breaking changes. The playground is an operator-opt-in feature via SANDBOX_ENABLED=true; default remains off.

What shipped

Public playground

The public playground at pulpengine.dev/playground went live with both modes:

  • JSON mode — CodeMirror 6 editors for template.json and data.json side-by-side with a live PDF preview. Paste a payload, edit a template, press Run. PDF drops in the right pane.
  • Visual mode — the full editor SPA inside an iframe via the <pulp-engine-editor> custom element. Full-width Editor | Preview tabs so the three-column layout (Blocks · Canvas · Properties) has room to breathe. Run auto-flips to Preview on success, stays on Editor on failure.

Both modes drive the same host-owned render through SandboxClient, so quota accounting, auto-remint on 401, and error mapping (400 / 422 / 429 / 503) behave identically regardless of which surface the user is in.

Other playground features:

  • Starter packs — Invoice, Letter, and Report fixtures. Re-usable from either mode.
  • Shareable URLs — Share button encodes template + data into the hash via pako-gzip + base64url. 6 KB cap with a “too big to share” fallback pointing at Template download. Invalid-hash banner + hash-clear via history.replaceState. Clipboard-denial fallback: banner with the URL in a read-only auto-selecting input so the user can copy manually.
  • Success signalling — checkmark icon + green background on the Share button during the 2s copied window; peripheral-vision detectable, not just a label swap.
  • Quota budget — 20 renders per 15-minute session, IP-pinned. Live chip in the toolbar.

Streaming SDKs

  • Go SDK streaming helper — hand-written packages/sdk-go/stream/stream.go. stream.Client.RenderStream() and RenderPreviewPdfStream() return a live io.ReadCloser instead of the generator’s *os.File (which pre-buffers into a temp file). StreamMode query selector, context.Context cancellation, *stream.HTTPError with parsed code/message/request_id before the caller sees the body. 9 unit tests cover query semantics, incremental arrival, cancellation, error-envelope parsing, and auth propagation.
  • .NET SDK streaming helper — hand-written PulpEngine.Sdk.Client.RenderStreamingClient. HttpCompletionOption.ResponseHeadersRead so PDFs stream end-to-end. StreamingResponse (IAsyncDisposable) wraps the live Stream. PulpEngineStreamException with StatusCode, Code, ServerMessage, RequestId. 8 xUnit tests against an in-process HttpListener.
  • Both sit alongside (not instead of) the generated clients; the generated RenderApi methods are unchanged.

Plugin API: observe-only post-render

ctx.onPostRenderObserve(hook) is a new plugin hook that fires on every render but cannot rewrite the response. Context carries durationMs, outputSizeBytes, pageCount, streamed — no writable output field.

The structural consequence is that observer hooks do NOT disable streaming on /render or /render/preview/pdf. Previously, registering any post-render hook (including pure-metrics ones) forced /render onto the buffered path. Now:

  • onPostRenderObserve(hook) — observe-only, streaming-safe. Use for metrics, audit trails, webhooks, per-render billing counters.
  • onPostRender(hook) — byte-mutating, forces buffering. Use when you need to rewrite the output (PDF watermark, add pages, wrap with cover sheet, etc.).

The stream_incompatible_hook 400 error message now explicitly names onPostRenderObserve as the correct fix for plugin authors who only wanted to observe.

Templates API: structural diff

POST /templates/:key/diff returns a structural diff between two stored template versions. Reuses diffTemplates() from @pulp-engine/template-diff directly — no CLI shell-out. The raw TemplateDiff envelope covers metadata, renderConfig, inputSchema, fieldMapping, document-root, and document-tree changes plus aggregate counts. Tenant-scoped via resolveTenant(); same auth as other read-only /templates/* routes. Returns 404 when either version is missing.

Exposed in:

  • TS SDKtemplates.diff(key, { beforeVersion, afterVersion })
  • Python SDKtemplates.diff(key, *, before_version, after_version)

CORS: custom response headers now reachable

The Fastify CORS plugin was registered without exposedHeaders, so browsers hid every custom response header from cross-origin JS. The sandbox playground’s quota chip silently stuck at 19/20 across multiple renders because X-Sandbox-Quota-Remaining was unreadable from the browser.

Now explicitly exposed:

  • X-Sandbox-Quota-Remaining + X-Sandbox-Page-Count — playground quota + page-count stream
  • X-Render-Duration-Ms + X-Render-Size-Bytes + X-Render-Pages — advertised in the “metered billing” homepage card; any browser-side SaaS integration can now read them
  • X-Request-Id — advertised for request correlation in docs/api-guide.md

Any other server-set headers stay hidden by default; the exposed list is the deliberate browser-readable surface.

Governing law: NZ, not Victoria, Australia

Licensor is based in New Zealand and intends to sell worldwide. The previous “State of Victoria, Australia” clause would have forced cross-border litigation to enforce the licence, required engaging Australian counsel, and misaligned with the jurisdiction the business actually operates under.

§10 Governing law now reads: “This Licence is governed by the laws of New Zealand… The courts of New Zealand have exclusive jurisdiction over any dispute arising from this Licence.”

§8 No warranty now opens with: “Except to the extent required by applicable law…” — generic enough to cover mandatory consumer protections in NZ (CGA 1993), UK (CRA 2015), EU (Directive 2011/83/EU), AU (ACL), etc., so the warranty disclaimer doesn’t contract out of rights it legally can’t.

§9 Limitation of liability (currently capped at AUD $100) left untouched pending a lawyer review — the currency mismatch is obvious after this change, and the bigger question of whether a fixed cap is the right structure deserves one coordinated pass, not a partial currency swap.

Both the repo-root EVALUATION-LICENCE.md and the site-mirror docs/evaluation-licence.md carry identical text. Top-of-file HTML comments remind future editors to keep the pair in sync until a content-collection loader replaces the mirror.

Landing page

Credibility pass — honest copy only

Absolute claims and adoption-implying framing were replaced with honest equivalents:

  • “Three things no other document engine gives you”“Why we built Pulp Engine this way”. The “no other” absolute was unprovable without exhaustive competitor research and any counter-example dents credibility.
  • “Early Adopter Programme · Now open” hero badge → “Free evaluation · $399/year commercial”. Retired the time-limited framing. Card 03’s EARLY ADOPTER tag similarly moved to NO CLOUD TAX, which echoes the metric strip’s 0 /render fees and reinforces the self-hosted + flat-pricing story.
  • “Three reasons engineering teams pick Pulp Engine”“Why we built Pulp Engine this way”. A brand-new product can’t claim teams “pick” it yet.
  • Meta description switched from time-limited to durable phrasing.

Metric strip

Four tiles, all backed by real counts:

  • 4 isolation modesin-process, child-process, container, socket. Replaces the previous 6 FORMATS tile which duplicated the hero heading (“One template. Six output formats.”). Matches the “Pick your blast radius” card further down.
  • < 250 ms p50 — DOCX/PPTX/XLSX client-side median, warm pool.
  • 20+ node types — tables, charts, pivots, barcodes, rich text, TOC, repeaters, conditionals, plus custom renderers. Backed by 20 entries in the editor’s block palette.
  • 0 /render fees — self-hosted, no metered cloud.

The broken bench-results-main link is gone; caption now points at /docs/benchmark-pack (which works).

Section reorder

New flow: Hero → MetricStrip → Design principles → SeeItRunning → TemplatesAreCode → TryItNow (with CodeTabs) → Seven more capabilities → Tertiary grid → Differentiators (4, trimmed) → Comparison table → RepoActivity → Quick-start → CTABanner.

The previous order dropped into curl snippets right after Design Principles, losing non-technical readers before they saw the editor. Visual demo now precedes code surfaces. TryItNow and the language CodeTabs (duplicated surfaces both showing “here’s a REST call”) merged into one section. Differentiators trimmed from 6 to 4; removed cards that duplicated MetricStrip or Comparison-table rows.

On-site routes, not GitHub blob URLs

Three footer/body links migrated from github.com/…/blob/main/X.md to on-site routes:

  • Deployment guide/docs/deployment-guide
  • Security/docs/security (SECURITY.md mirrored into docs/)
  • Evaluation Licence/docs/evaluation-licence (EVALUATION-LICENCE.md mirrored)

GitHub links kept where they belong: repo root, /issues, /releases, clone URL in quickstart, Edit on GitHub per-doc links, and the raw openapi.json download.

Embed snippet + quickstart fixes

  • <pulp-engine-editor src="/editor/embed">api-url="https://your-api.example". The real custom element uses api-url, not src. Anyone copy-pasting the broken snippet would have hit a dead embed.
  • cd pulp-enginecd pulpengine. The repo is TroyCoderBoy/pulpengine (one word); git clone creates pulpengine/, not pulp-engine/.
  • Removed the trailing . from the wordmark — it read like a typo at first glance.
  • Header now shows the DOCUMENT RENDERING subline under the wordmark (already was visible in the footer).

Operator notes

  • Playground deployment: set SANDBOX_ENABLED=true, SANDBOX_TOKEN_SECRET=<32-byte hex>, RATE_LIMIT_STORE=redis, and SANDBOX_ALLOWED_ORIGINS=<your playground origin>. See .env.file-mode.example for the full block. Default is off.
  • Licence key documentation: PULP_LICENCE_KEY now appears as a commented-out entry in every env example. Any non-empty value suppresses the evaluation watermark (Stage-1 policy; signed-key scheme deferred to Stage 2).
  • CORS-exposed headers: if you depend on any custom server header from a browser client, make sure it’s in the exposedHeaders list in apps/api/src/server.ts. The current list covers the six headers we document.
  • Governing-law change: applies to all new evaluation licences from this release. Deployments under previous versions continue to reference the text bundled at their commit — no automatic update.

Migration

No breaking changes. Upgrade by pulling the new container image or re-running the install pipeline. All SDK signatures are additive:

  • Go: new github.com/TroyCoderBoy/pulp-engine-go/stream sub-package — only imported if you opt in
  • .NET: new RenderStreamingClient class — only instantiated if you want streaming
  • TS + Python: new templates.diff(…) method — only called if you want diffs
  • Plugin API: new ctx.onPostRenderObserve(…) — existing ctx.onPostRender(…) unchanged

No schema migrations, no env-var renames, no behavioural changes to existing routes.