How NilPDF is built to keep your documents on your device, and how to report a problem.
Every PDF tool runs inside a Web Worker in your browser, executing Python compiled to WebAssembly via Pyodide. There is no NilPDF backend that accepts uploaded documents — the application has no server-side code capable of receiving a PDF file. This isn't a policy choice layered on top of a server that could technically receive files; the capability to upload a document simply doesn't exist in the code.
When you load nilpdf.com, your browser makes requests for:
Once a tool is open and you select a file, no further network requests are made to process it — everything after that point runs locally. You can verify this yourself; see Verify local processing below.
You don't have to take this on trust — you can confirm it directly in your browser:
If you ever observe a request that appears to transmit file content, please report it — see Vulnerability reporting below. That would be a bug, not intended behavior.
In the Network tab, once a tool is open, you should only see requests like these — none of which include your file:
| Request | Contains your file? |
|---|---|
| GET pdf.worker.min.js (cdnjs) | No — a fixed library file |
| GET pyodide.asm.wasm (jsDelivr) | No — a fixed runtime file |
| GET *.whl package files (jsDelivr) | No — fixed Python package files |
You should not see any POST or PUT request containing binary data around the time you select or process a file.
Your device │ ├─ Browser reads the PDF file you selected (stays on device) │ ├─ Web Worker runs Pyodide (WebAssembly Python) ── all local, no network │ │ │ └─ pypdf / reportlab / Pillow process the file in memory │ └─ Result written to a new file → offered to you as a download Network requests (separate from the above): Browser → NilPDF hosting / CDNs : page HTML/CSS/JS, pdf.js, Pyodide runtime (no step above ever sends your file, filename, or extracted content anywhere)
NilPDF currently relies on same-origin hosting plus Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp headers, which are required to enable SharedArrayBuffer for Pyodide's WebAssembly runtime. A formal Content-Security-Policy header restricting script sources is not yet deployed; this is tracked as follow-up work.
| Component | Purpose |
|---|---|
| Pyodide | Runs the Python PDF-processing engine as WebAssembly, in-browser |
| pypdf | PDF parsing, merging, and manipulation (Python) |
| reportlab | Generating overlays (watermarks, page numbers, redaction boxes) |
| Pillow | Image handling for PDF-to-image and image-to-PDF conversions |
| pdf.js | Client-side PDF rendering and page previews |
Python dependency versions are pinned in requirements.txt for the project's automated test suite and updated via Dependabot. The production Pyodide runtime resolves the same package names independently through Pyodide's own package index at load time.
When you enter a password to open an encrypted PDF, it is used only in-browser, in memory, to decrypt the file locally for processing. The password is never transmitted anywhere. Refreshing or closing the tab clears it from memory.
If you discover a security issue, please report it through the feedback form on the site, or via the contact details on the About page. Please avoid publicly disclosing a vulnerability until it has been addressed.