Ecosystem use cases
Five production architectures, each built from parts the ecosystem already ships. Store a couple of kilobytes of JSON instead of hundreds of kilobytes of PDF and render on the reader's own device; sign to PAdES B-LTA across an air gap; gate template changes in CI with a real document diff; serve edge-rendered PDFs with honest HTTP caching; and swap in zipnative, the sibling ZIP engine, as a deterministic DEFLATE codec wherever the engine runs. Every arrow in the diagrams below is a public, shipped API — nothing here is aspirational.
The capability × surface matrix tells you what each surface can do; this guide shows how the surfaces compose into systems. Each case names its exact building blocks, shows the load-bearing code, and states its limits. Three companion pages take one surface each and go deeper on the releases of September 2026: CLI use cases (pdfnative-cli v1.5.0), MCP use cases (pdfnative-mcp v1.7.0) and React use cases (pdfnative-react v1.3.0).
Case 1 — Store the spec, not the PDF#
A typical generated PDF weighs hundreds of kilobytes — most of it embedded
fonts and compressed streams that are recomputed identically on every
render. The document's actual information content is the text and structure,
which fits in a few kilobytes of JSON. So: persist the compact
DocSpec (or a
DocumentParams JSON), and let the device that asks for the PDF produce it.
Why this works in a browser at all: the engine has zero dependencies and no Node-only code paths on this route. Rendering is synchronous and the output is a valid, uncompressed PDF — and since the bytes stay on the device, their size costs nothing on the wire.
// On the reader's device — nothing but the two packages and React.
import { renderSpecToBytes, downloadBlob } from 'pdfnative-react';
const spec = await fetch(`/api/documents/${id}`).then((r) => r.json()); // ~2 KB
const bytes = renderSpecToBytes(spec); // rendered locally, synchronously
downloadBlob(bytes, 'invoice.pdf'); // builds the Blob itself
Want compressed output too? setDeflateImpl accepts a synchronous deflate
function whose result is written into the file verbatim, so it must produce a
zlib-wrapped (RFC 1950) stream — /FlateDecode is zlib per ISO 32000-1
§7.3.8. The browser's CompressionStream cannot be plugged in (it is
asynchronous by nature); a tiny synchronous library can:
import { zlibSync } from 'fflate'; // synchronous, zlib-wrapped output
import { setDeflateImpl, renderSpecToBytes } from 'pdfnative-react';
setDeflateImpl((bytes) => zlibSync(bytes));
const compressed = renderSpecToBytes({ ...spec, layout: { ...spec.layout, compress: true } });
What you gain, concretely:
- Storage — a 2 KB spec versus a 300 KB PDF is a ~150× reduction, before database compression. A million archived invoices stop being a storage line item.
- Bandwidth and latency — the payload that crosses the network is the JSON, not the PDF.
- Diffability — specs are plain data: version them, diff them, patch a typo without a byte-level regeneration pipeline.
- Re-branding without a migration — yesterday's specs render with today's template and fonts.
Honest limits: the client re-renders with the engine version it has, so a spec is not a byte-frozen archival artefact — for documents that must never change (or that carry signatures), render once and archive the bytes (see Case 2). Non-Latin scripts need their font modules delivered to the client (each is a lazily imported module; ship only what the spec's languages need). The engine renders identical bytes for identical input on the same version — pin the engine when reproducibility matters.
Case 2 — Air-gapped PAdES B-LTA archiving#
Long-term signature validation has a paradox: the evidence that keeps a signature verifiable for decades (OCSP responses, CRLs, timestamps) comes from the network — but the most sensitive signing environments are exactly the ones with no network. CLI 1.4.0 resolves it by splitting the ladder: evidence is collected as replayable JSON on a connected machine, and embedded fully offline inside the enclave.
# Connected zone — sign with a timestamp (B-T), then gather the evidence.
# --profile pades → ETSI.CAdES.detached; --timestamp → RFC 3161, SSRF-guarded.
pdfnative sign --input contract.pdf --output signed.pdf \
--profile pades \
--timestamp https://tsa.example.com/rfc3161
pdfnative ltv collect --input signed.pdf --online --output ltv-data.json
# ── controlled transfer of ltv-data.json across the air gap ──
# Enclave — no network I/O is even possible in `embed`, by design.
pdfnative ltv embed --input signed.pdf --data ltv-data.json --output signed.lt.pdf
# Gateway (network) — cap the ladder with a document timestamp (B-LTA).
pdfnative doc-timestamp --input signed.lt.pdf \
--url https://tsa.example.com/rfc3161 --output signed.lta.pdf
# Anywhere, later — verify the whole ladder offline from the embedded /DSS.
pdfnative verify --input signed.lta.pdf --strict --revocation offline
--profile pades is not optional here: the default is pkcs7, which writes
/SubFilter /adbe.pkcs7.detached — a valid CMS signature, but not an
ETSI EN 319 142-1 baseline, so the whole B-T → B-LTA ladder would sit on a
non-PAdES foundation.
The evidence file is versioned JSON (pdfnative schema ltv-data) with DER
payloads as base64 — auditable before it crosses the boundary, replayable if
the embed step must be re-run. Every network touch on the connected side is an
explicit per-invocation opt-in behind an SSRF guard (http/https only, private
addresses blocked, no redirects). Years later, run doc-timestamp again to
renew the protection before the TSA certificate expires — earlier revisions
stay byte-identical.
Honest limits: ltv embed trusts the evidence it is given — the transfer
process, not the tool, is what makes the air gap meaningful. The MCP surface
offers the same ladder (add_ltv, timestamp_pdf) with operator-configured
transport instead; the React renderer deliberately does not sign (post-process
its output with the CLI or the library).
Case 3 — A document-regression gate in CI#
Templates drift. A dependency bump changes a wrap point, a copy edit pushes a
table onto page 3, a refactor silently drops the footer — and nobody notices
until a customer does, because PDFs in CI are write-only artefacts nobody
opens. CLI 1.4.0's compare turns them into asserted artefacts: exit 0 when
the golden and the candidate agree, exit 1 with a diff report when they do
not.
# .github/workflows/documents.yml
- name: Render candidate (hard-fail on PDF/A diagnostics)
run: |
pdfnative render --input templates/invoice.json \
--output out/candidate.pdf --tagged pdfa2b --strict
- name: Diff against the golden file
run: |
pdfnative compare golden/invoice.pdf out/candidate.pdf \
--mode both --ignore-whitespace --format json
# identical → exit 0 · different → report on stdout, then exit 1 / E_CHECK_FAILED
The both mode diffs the extracted reading-order text and the declared
structure — page count, page and print boxes, metadata, form fields,
annotations, encryption, signatures — with a --tolerance in points for
geometry. render --strict upgrades PDF/A diagnostics (an unembedded form
font, a DeviceCMYK image under the wrong profile) from warnings to a failed
build before the first output byte. When a change is intentional, promoting
the candidate to golden is a one-line copy in the same PR.
Honest limits: compare is not a visual diff — pdfnative has no
rasteriser, so pixel-level rendering changes that alter neither text nor
structure pass; pair it with an external rasteriser if that matters to you.
Note that engine upgrades can legitimately change bytes without changing
content (the diff compares content, not bytes — which is exactly why it works
across engine versions where a checksum would not).
Case 4 — Edge-rendered PDFs with honest HTTP caching#
Rendering PDFs in a central service means queues, SaaS round-trips, or a
container with a headless browser. The engine's zero-dependency, Fetch-native
design removes the constraint: renderToResponse returns a web-standard
Response, so the same code runs in a Next.js route handler, Cloudflare
Workers, Deno Deploy or Bun — and since pdfnative-react 1.2.0 it speaks real
HTTP caching.
// app/invoice/[id]/route.tsx — identical on Workers / Deno / Bun.
import { renderToResponse } from 'pdfnative-react';
export async function GET(_req: Request, { params }: { params: { id: string } }) {
const data = await loadInvoice(params.id);
return renderToResponse(<Invoice data={data} />, {
fileName: `invoice-${params.id}.pdf`,
cacheControl: 'public, max-age=300, s-maxage=86400',
etag: true, // strong validator derived from the rendered bytes
});
}
cacheControl is emitted verbatim, so the CDN tier does the heavy lifting;
etag: true derives a strong validator from the actual bytes, so conditional
requests revalidate correctly and a 304 Not Modified costs no render.
Streamability is validated before the first byte — a document that cannot
stream (a table of contents, {pages} placeholders) fails up-front instead of
mid-response with the headers already sent.
Honest limits: etag: true must buffer the response to hash it — for very
large documents choose either streaming (constant memory, no strong ETag) or
the buffered validator, not both. A per-user document should say
private in its Cache-Control, and remember the PDF inherits whatever
authorisation bug the route has — cache keys must include whatever
distinguishes users.
Case 5 — Swap the compressor: zipnative as pdfnative's DEFLATE codec#
pdfnative ships no compressor of its own. In Node — and in Bun and Deno 2,
which expose node:zlib — initNodeCompression wires the native deflate; a
browser or a Worker has no built-in synchronous deflate, so compress: true falls back to stored blocks — a
valid PDF, but not a smaller one — until you inject one. Case 1 does that
with fflate's zlibSync through setDeflateImpl.
zipnative is the sibling zero-dependency ZIP engine,
and its pure-TypeScript DEFLATE encoder is synchronous, deterministic and
runtime-agnostic — precisely what setDeflateRawImpl expects: raw RFC 1951
output, to which pdfnative adds the zlib envelope itself.
import { getCodec, METHOD_DEFLATE } from 'zipnative';
import { setDeflateRawImpl, buildDocumentPDFBytes } from 'pdfnative';
const codec = getCodec(METHOD_DEFLATE); // ZipCodec | null
if (!codec?.compressSync) throw new Error('zipnative DEFLATE codec unavailable');
setDeflateRawImpl((buf) => codec.compressSync!(buf, { level: 6, deterministic: true }));
const bytes = buildDocumentPDFBytes({
title: 'Invoice',
blocks: [{ type: 'paragraph', text: 'Compressed by zipnative, framed by pdfnative.' }],
layout: { compress: true, creationDate: new Date('2026-01-01T00:00:00Z') },
});
What you gain, concretely:
- Real compression everywhere the engine runs — the same call in a browser or a Worker, byte-identical to Node, Bun and Deno when they use the same codec — while the whole stack stays dependency-free: zipnative has no runtime dependency either.
- Determinism —
deterministic: truepins zipnative's pure encoder, so with a pinnedcreationDatethe compressed PDF is byte-identical on every runtime, not just the uncompressed one. The CLI, MCP and React equivalents of the date pin are listed in CLI use cases, Case 1. - Composition — Case 1's on-device render can take a deterministic compressor from the same zero-dependency family instead of fflate, and Case 4's edge responses shrink without a native module in the runtime.
Honest limits: zipnative is one more package to install and, in Case 1, one
more module to deliver to the device. setDeflateRawImpl is process-global
state — set it once at start-up, and use it rather than setDeflateImpl,
which since pdfnative 1.8.0 rejects raw DEFLATE output. A zipnative build and
a node:zlib build of the same document differ in bytes (different encoder,
identical content), so choose one compressor per pipeline and rebaseline
once. A pure-TypeScript encoder is slower than native zlib; on a Node server
with no cross-runtime requirement, initNodeCompression remains the
default choice.
Then archive it. The reverse composition — encrypt with pdfnative first, because ZIP's own encryption is not worth relying on, then package the ciphertext in a deterministic ZIP with zipnative — is documented on zipnative's side: Encrypt first, then archive.
Picking parts, not a platform#
The five cases share one property: each is assembled from surfaces that also
work alone, so none of them locks you in. The specs of Case 1 render fine
server-side the day you need a byte archive; the golden files of Case 3 are
ordinary PDFs any tool can open; the ladder of Case 2 verifies in Adobe
Acrobat, not just in pdfnative verify; the codec of Case 5 unplugs with
one call. Start with the case closest to your bottleneck and borrow pieces
from the others as they become relevant.
See also#
- Choosing your surface — the capability × surface matrix behind every arrow above.
- Long-term validation — the PAdES ladder in engine terms.
- Self-verifying generation — the assert-your-own-output loop that pairs naturally with Case 3.
- React guide —
DocSpec, rendering entry points and the HTTP caching options. - CLI guide — the complete v1.5.0 command reference.
- CLI use cases · MCP use cases · React use cases — one surface each, on pdfnative-cli v1.5.0, pdfnative-mcp v1.7.0 and pdfnative-react v1.3.0.
- zipnative use cases — the sibling ZIP engine's architectures, including the encrypt-then-archive pipeline — the opposite direction of Case 5.