Every hour, a script sends a document with a unique marker through our public API, then searches our logs, temp files, containers and database for it. If it finds the marker anywhere, the check fails, and the latest result is public on pagenull.dev/proof.
Almost every PDF API says it doesn't keep your documents. Most mean it. But a promise in a privacy policy is hard to check, and the invoices, contracts and payslips people send to these APIs are exactly the documents you don't want sitting in someone's log files.
So when we built pagenull, an HTML to PDF API, we set ourselves a stricter goal: don't just say nothing is stored. Build it so nothing can be, test that on every change, check it against production every hour, and publish all of it. This post is how that works, and where its limits are.
Where a PDF API can quietly keep your document
The obvious place is a database or a storage bucket. The less obvious places are where documents usually leak:
• Chromium's own files. Headless Chromium writes a profile, a disk cache and temp files. A page you render can end up in any of them.
• Logs. One console.log(req.body) while debugging, or an error handler that logs the failing request, and your invoice is in a log aggregator for 30 days.
• Crash dumps. When Chromium crashes, its crash reporter can write a memory dump, which may hold the page.
• Leftover options. A "store this PDF for 24 hours" or "debug mode" flag that someone added once and nobody removed.
• Delivery. Uploading the PDF somewhere on the customer's behalf, and keeping a copy along the way.
None of these need bad intent. They are what happens by default. So our setup starts by closing each one.
The setup: nowhere to write
The checks later in this post only confirm the setup. The setup is what actually protects the data.
• Read-only filesystem. The renderer container can't write to disk. Chromium's only writable folder is /tmp, and that lives in memory (tmpfs), so it's gone when the renderer restarts.
• A fresh incognito context per render. Each request gets its own browser context, closed as soon as the PDF is done. Nothing carries over between customers.
• Crash reporter off. No crash dumps that could hold a page.
• A logger that only accepts metadata. Our logger takes a fixed set of fields: key id, time, endpoint, status, size and duration, plus a render id you can quote to support. Anything else is dropped.
• No storage option at all. No parameter makes pagenull keep a document: no file links, no "log this request", no debug retention. If you want the PDF somewhere else, deliverTo uploads it to your own storage and we keep nothing. It also refuses private and cloud metadata addresses, so it can't be used to reach our internal network.
• Hosted in the EU. The renderer runs at Hetzner in Helsinki, Finland.
The check: a marker through production, every hour
Every check we run works the same way. Put a unique marker in a document, render it, then look everywhere it could have been kept. If the marker turns up anywhere, the check fails.
Once an hour, a script on the server does this against the live service:
- Generate a fresh random marker and put it in an HTML document.
- Send it through the public API as a free test render, like any customer would.
- Search for the marker in: ◦ the test render's response and records ◦ the container logs ◦ the renderer's temp files ◦ every file written by any container ◦ a dump of the database
- Publish the result on /proof. A test render takes the same path as a paid one: the same API route, renderer, logging and receipt. The only differences are a watermark on the page and that it doesn't count toward a plan. Why a marker instead of just looking for files? Because "no new files" says nothing about logs, and "logs look clean" says nothing about a database column. A random string that only exists in one request is easy to search for everywhere, and impossible to find by accident. The tests: before every change we ship
The hourly check catches problems in production. The test suite stops them reaching production. It runs before every deploy, and the code is published on /proof as it is.
• no-retention.test.ts renders documents containing a marker through the real API code. It then searches a full dump of the database, every log line and all console output, and fails if any file is written during the render.
• no-trace-on-disk.test.ts renders a marked document in a real Chromium. It searches every file Chromium created (profile, cache, temp files) while the browser runs, and again after it closes.
• no-write-paths.test.ts reads our own source code. It fails if anything writes to disk outside a short allowlist, logs outside the metadata-only logger, or brings back an option or table for storing output.
The last one matters most over time. The first two check today's behaviour. This one guards against a future change, for example a well-meaning "let's keep failed renders for debugging", getting merged without anyone noticing.
Signed receipts: proof of what we made
Every render comes back with a pagenull-receipt header. It's an Ed25519 signature over:
• the render id
• the SHA-256 of the PDF
• the time and the region
• the renderer build (a hash of the renderer's code, dependency versions and Chromium version)
• stored: false
Anyone can check a receipt against our public key at /.well-known/pagenull-keys.json, on pagenull.dev/verify or with a short Node.js script, without asking us. If someone changes even two digits of the bank account on an invoice, the hash no longer matches.
To be precise about it: a receipt proves what we produced, when, where and with which build. It doesn't prove we deleted anything.
What this can't prove
Nothing can prove a negative from the outside, and we'd rather say that than pretend otherwise.
• The hourly check shows that the running service didn't keep the marker. It can't show that we didn't change the code an hour later.
• The tests show what the code does at the commit they ran on. They don't stop us from deploying something else.
• The receipt proves the output. It says nothing about what happened after.
So the claim rests on several things together: an architecture with no path to write your content, a read-only renderer, published tests, an hourly check against production, and a data processing agreement that puts it in writing. Each one alone is weak. Together, cheating would mean deliberately breaking all of them while publishing results that say otherwise.
Check it yourself You don't need an account for any of this:
• /proof: the latest hourly result, the published tests, and a sample e-invoice with its validator report.
• /verify: paste a receipt, drop in the PDF, and check both in your browser.
• Playground: paste HTML, get a PDF and its receipt, no sign-up.
If you find a way our setup could still keep a document, we want to hear it. That's the point of publishing all this.
pagenull is an HTML to PDF API hosted in the EU, free for your first 100 PDFs a month. Get a free API key.

