Skip to content

Security ​

BroadPaper is designed to run inside enterprise software where templates are authored by end users and data is sensitive.

No HTML in, no HTML out ​

Rich text is stored as a structured model (paragraphs, marks, tokens), never as HTML. Rendering produces DOM from that model with text nodes and attribute setters; innerHTML is never used with user content. Pasting into the inline editor accepts plain text only. Custom block renderers return virtual nodes that we serialise, so a block cannot emit raw markup by accident.

No code execution ​

The expression language is parsed with its own tokenizer and Pratt parser and evaluated by a tree-walking interpreter. There is no eval, no Function, no template compilation to JavaScript. Property access reads own data properties only: client.constructor, __proto__ and function members resolve to undefined. Expressions have length and nesting limits. Runtime data problems produce empty output and warnings, never exceptions that abort a render.

URLs ​

Images, logos, links and buttons accept http(s):, mailto:, relative paths and data:image/* URLs. javascript: and other schemes are rejected at validation and again at render. Inline SVG data URLs are refused if they contain scripts, event handlers or a foreignObject — after being decoded, whether they are percent-encoded or base64, and over the whole payload rather than a prefix. That matters because an uploaded file always arrives as base64: an image the designer accepts is one the renderers accept.

The PDF service ​

  • Each render uses a fresh, isolated browser context.
  • All network requests are intercepted. Only data: URLs and hosts on the allow-list may load, and private, loopback, link-local and CGNAT ranges are blocked even for allowed hostnames after DNS resolution (SSRF protection). Request counts are capped.
  • Timeouts, maximum page counts and request body sizes are enforced; a shared-secret bearer token gates the endpoint.
  • The page never executes template-supplied JavaScript because templates contain none; the only scripts are BroadPaper's runtime and, optionally, the host's own custom block bundle.
  • Metadata is written with pdf-lib; no shell commands are involved anywhere.

The browserless renderer has no browser and no network stack of its own: it draws what it is given. Assets referenced by URL have to be fetched by your code and handed over as bytes or data: URIs, which means the allow-listing decision is yours and is made before anything is fetched.

Data locality ​

BroadPaper has no SaaS component, no vendor telemetry and no licence check that phones home. Nothing about your templates, your schemas or your data is sent anywhere by the SDK, and there is no endpoint of ours for it to be sent to. Report data is bound in the user's browser (editor, preview, print fallback) or in your own renderer.

That is a claim about our code, not about the page it runs on. The network is used when you ask for it, and these are the ways to ask:

What you configureWhat it fetches, and from where
A theme.fonts entry (or a fonts entry) with a URLThe font files, from that host: by the browser for the canvas, and by whichever process draws the PDF — the render service only from its allowedHosts.
A logo, image or chart asset with an http(s) URLThat asset, from that host.
pdfService: { url }The template, the data and the theme are POSTed to that URL — your service, but a network call with your customer's data in it. Give it an https:// address: see transport below.
Your own onExportPdf, createMeasurer or data-loading codeWhatever you make it fetch.
Custom blocksYour code, in your trust domain.

Transport ​

The render service listens with Node's http module and has no TLS of its own — terminating it is the ingress's job, and doing it in two places means rotating certificates in two places. Where a request crosses a network you do not control, put TLS in front of the service and give clients the https:// address.

This matters more than it usually would, because the service authenticates with a shared bearer token: in clear text, that token is the credential, and anything that can read one request can replay every other. The same applies to pdfService: { url } in the browser, which sends your customer's data as well as the token.

Declare fonts in theme.fonts as data: URIs or same-origin files rather than hosted addresses, use the same for other assets, and a render makes no outbound request at all. The Chromium PDF service blocks everything not on an explicit allow-list precisely so that this is enforceable rather than merely intended.

Custom blocks ​

Custom blocks are your code and run in your trust domain. Keep render functions pure, validate any URLs you emit with isSafeUrl, and prefer theme tokens to user-supplied colours.

Licensing and third-party notices ​

Everything BroadPaper depends on at runtime is under a permissive licence — MIT, ISC, Apache-2.0 — with no copyleft anywhere in the tree. Two of those licences require their notice to travel with the software, and one of the two is easy to miss.

The browserless engine is a single WebAssembly binary. Statically linked into it are its whole Rust dependency tree and two complete fonts — Noto Sans Regular and Bold, under the SIL Open Font License 1.1. Neither is visible from a package.json. A subset of Noto is embedded into any PDF containing a character the standard PDF faces cannot represent: a Greek or Cyrillic name, a symbol. Documents produced that way carry no restriction — the OFL explicitly exempts anything created with the font — but the font software inside the engine does, so the notice has to ship with anything that ships the engine.

THIRD-PARTY-NOTICES.md carries all of it, and is generated by reading the installed tree and the engine binary: crate names and versions out of the compiled artefact, font licences out of the fonts' own name tables. It is regenerated by our build, ships inside every published package, and is served beside the demo — a browser bundle strips the npm metadata that would otherwise carry a licence along with the .wasm.

If a future version of the engine embeds a font under a licence you cannot accept, the generator is what will tell you: it fails rather than emitting a notice it could not read.

The Chromium backend is different. playwright-core is Apache-2.0 but downloads a browser build separately at install time, and that browser carries its own licences, which are not restated here. An image built on that backend needs to carry Chromium's notices too. The browserless engine has no such tail.