Assembly
Turning documents into one report
A web app that assembles formal client reports.


The problem
The team at a consulting firm assembled every formal client report by hand: a cover page, then appendix dividers, then the documents for each appendix, merged into one PDF. The same nine report structures, over and over, with different content each time.
Done by hand, it was slow and easy to get wrong: pages out of order, numbering that restarted halfway through, an appendix missing. Every project deliverable paid that cost again.
What I built
A web app that turns report assembly into a short flow:
Configure type → Create report → Upload documents → Generate → Download
Each report type is set up once with its cover pages. After that, someone creates a report, drops PDFs or Word documents into the appendix slots, reorders them if needed, and generates one finished PDF with continuous page numbering throughout.
How it works
Three services: a Next.js front end, a FastAPI back end, and Gotenberg, a document-conversion service that runs LibreOffice behind a clean HTTP API. The work happens in three flows, at three different times:
- Set up, once per report type. Each type gets its cover pages, uploaded as PDFs or generated from a template.
- Upload, for every document. The original is stored as-is. Word files go to Gotenberg and come back as PDF. Cleaning old page numbers and footers is a step the user chooses, with a preview first.
- Generate, on every preview or download. The API gathers the report's files in order, adds the cover pages, merges them, and stamps one "Page X of Y" footer across the whole document. It returns a single PDF or a ZIP of sections.
The highlighted path follows a document from upload to the finished report.
A few of the decisions:
- Merge on demand, never store the result. The finished report is rebuilt from its parts each time. There's no stale copy to invalidate when a document changes, and nothing to clean up.
- Conversion in its own container. Running LibreOffice inside the API would have bloated it and tied the two together. Gotenberg keeps the back end slim and handles concurrent conversions on its own.
- Pinned to an exact version. A LibreOffice update can shift how a document renders. Pinning Gotenberg to one version means a report generated next month looks the same as one generated today.
- Stamp the footers ourselves. Gotenberg can merge PDFs but can't add custom footers, so merging and page numbering happen in Python with pypdf and a reportlab overlay.
- SQLite, on purpose. It's a single-team internal tool. SQLite meant no extra service to run, and SQLAlchemy keeps the move to Postgres a small change if it's ever needed.
What was hard
Page numbering sounds trivial until the source documents already have their own. Stamping "Page 7 of 42" over a document that already says "Page 1" produces a mess, so cleaning exists as its own step, with a preview, for the documents that need it.
What I learned
The most valuable software isn't always the most complex. Assembly does one narrow job for one team, and it removed a task people repeated on every project. Understanding that job precisely was the real work; the build was small.