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.jsonanddata.jsonside-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-widthEditor | Previewtabs 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 + datainto 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 viahistory.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()andRenderPreviewPdfStream()return a liveio.ReadCloserinstead of the generator’s*os.File(which pre-buffers into a temp file).StreamModequery selector,context.Contextcancellation,*stream.HTTPErrorwith parsedcode/message/request_idbefore 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.ResponseHeadersReadso PDFs stream end-to-end.StreamingResponse(IAsyncDisposable) wraps the liveStream.PulpEngineStreamExceptionwithStatusCode,Code,ServerMessage,RequestId. 8 xUnit tests against an in-processHttpListener. - Both sit alongside (not instead of) the generated clients; the generated
RenderApimethods 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 SDK —
templates.diff(key, { beforeVersion, afterVersion }) - Python SDK —
templates.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 streamX-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 themX-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.
Legal
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 ADOPTERtag similarly moved toNO CLOUD TAX, which echoes the metric strip’s0 /render feesand 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 modes —
in-process,child-process,container,socket. Replaces the previous6 FORMATStile 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 intodocs/) - 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 usesapi-url, notsrc. Anyone copy-pasting the broken snippet would have hit a dead embed.cd pulp-engine→cd pulpengine. The repo isTroyCoderBoy/pulpengine(one word);git clonecreatespulpengine/, notpulp-engine/.
Logo
- Removed the trailing
.from the wordmark — it read like a typo at first glance. - Header now shows the
DOCUMENT RENDERINGsubline 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, andSANDBOX_ALLOWED_ORIGINS=<your playground origin>. See.env.file-mode.examplefor the full block. Default is off. - Licence key documentation:
PULP_LICENCE_KEYnow 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
exposedHeaderslist inapps/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/streamsub-package — only imported if you opt in - .NET: new
RenderStreamingClientclass — only instantiated if you want streaming - TS + Python: new
templates.diff(…)method — only called if you want diffs - Plugin API: new
ctx.onPostRenderObserve(…)— existingctx.onPostRender(…)unchanged
No schema migrations, no env-var renames, no behavioural changes to existing routes.