PDF Merger
Please note that some project links may be temporarily unavailable.

Web Dev
Full Stack
Frontend
PDF Merger

Tech Stack

TypeScript
React
Vite
Tailwind CSS
shadcn/ui
Web Workers
Content Security Policy
C#
.NET
ASP.NET Core
Minimal APIs
OpenXML SDK
Docker
Cloudflare Workers
Fly.io
Vitest
CI/CD
Unit Testing
RESTful APIs

Description

Most "merge PDF" sites upload your documents to a server, process them there, and promise to delete them afterwards — which is how contracts, payslips, medical records, and passports routinely end up on someone else's infrastructure. PDF Merger removes the need to trust that promise: the merger cannot upload anything, because there is nowhere for the files to go. The claim is designed to be falsifiable in about ten seconds — open DevTools, merge some files, and watch the network panel stay empty after the page loads.

That guarantee is enforced by the browser, not by convention. The deployed site ships a per-page Content Security Policy in which the merge page's connect-src is 'self' alone, so even a bug could not send a PDF to a third party. Fonts are bundled into the build rather than pulled from Google Fonts, so even the initial page load touches no third party. Each tool is a separate HTML document rather than a route in an SPA — that is precisely what allows a different policy per page, and what keeps the one exception from spreading.

The Word tool is that exception, and it is scoped rather than general. It posts to a .NET 10 Minimal API that merges .docx files with the OpenXML SDK, holding every document in a MemoryStream from first byte to last — nothing is written to disk, nothing is stored, and the log line carries a document count and a byte total, never a filename. It ships as a chiselled ASP.NET container whose Docker build runs the xUnit suite before publishing, deployed to Fly.io with scale-to-zero and a health check, and the allowed browser origin lives in a secret rather than the committed config so it cannot drift.

The part I am most pleased with is that the privacy claim is checked by the build rather than maintained by discipline. A single environment variable is the source of truth for the Word service origin: a Vite plugin generates the CSP exception from it, and a build script reads the compiled headers back and fails the build unless the merge page is on connect-src 'self' alone and the Word page names exactly that one origin and nothing else. An unconfigured build gets no exception at all, which is correct — its Word page has nowhere to post either. Both failure modes were verified by deliberately breaking them.

  • Built a zero-upload PDF merger where the privacy guarantee is enforced by a per-page Content Security Policy (connect-src 'self'), not asserted in a privacy policy.
  • Moved all pdf-lib work into a single Web Worker so multi-second parses never block the UI thread, keeping the merge logic as pure functions that are testable in Node.
  • Wrote the Word merge service as a .NET 10 Minimal API in C# using the OpenXML SDK, merging in-memory only — no disk writes, no storage, no identifying data in logs.
  • Shipped the service as a chiselled ASP.NET container whose Docker build runs the xUnit suite before publishing, deployed on Fly.io with scale-to-zero and a /health probe.
  • Made the security claim build-enforced: one env var generates the CSP exception, and a post-build script fails the build if either page's policy drifts — verified by deliberately breaking both cases.
  • Chose a multi-page build over an SPA specifically so each tool carries its own CSP, containing the one uploading page instead of loosening the policy site-wide.
  • Deployed as a Cloudflare Worker serving static assets with SPA fallback disabled, so unknown paths return a real 404 rather than a soft 200 — a bug caught on the first deploy.
  • Handled failure honestly in the UI: unmergeable files stay listed with a specific reason (corrupt, encrypted, wrong type) rather than disappearing silently.
  • Kept the whole surface typed and tested with TypeScript 6, Vitest unit tests, and oxlint, with configuration committed to the repo rather than left in a hosting dashboard.

Page Info

In-Browser PDF Merge

Drop files in, drag them into order, merge, download. All PDF work — page counts and the merge itself — runs in a single Web Worker using pdf-lib, so parsing a large document never freezes the tab and the UI thread never touches the library. There is no backend on this page: load it, disconnect from the network entirely, and it still works. Files that cannot be merged (corrupt, password-protected, or a .txt renamed to .pdf) stay in the list with the reason shown instead of vanishing.

/projects/pdf-merger/merge_pdfs.webp

Word Merge — the Named Exception

Merging WordprocessingML in a browser produces files that open and are subtly wrong: styles, numbering, and lists from one document silently taking over another. Rather than ship that, this one tool posts to a dedicated C#/.NET service that merges via the OpenXML SDK. The page discloses the upload above the dropzone, before any file is added — the disclosure is read from per-page config, so a page that uploads cannot inherit the promise the other page makes.

/projects/pdf-merger/word_merge.webp