Compliance Reporting Platform
The report looked perfect. Then we deployed it to a client with real data and watched the pod ceiling disappear.
- Frontend Engineer
- Compliance Engineering
- 2024
The Brief
Compliance reports needed to go out to customers — formatted, branded, printable PDFs. The backend is Go. The obvious path was a Go PDF library or a template renderer. We looked at it and quickly ruled it out.
The design team had built detailed Figma layouts — multi-page reports with section headers, severity badges, data tables, coverage charts. Replicating that fidelity in a Go PDF template would've meant hand-coding every pixel of layout in a library that doesn't do CSS. That wasn't a week of work, it was a month, and it would break every time the design changed.
The question wasn't how to generate a PDF. It was how to turn a Figma design into a PDF without rebuilding it from scratch every sprint.
The Approach
We built it as a web page instead. The Go backend generates the report data, serializes it to JSON, creates a short-lived auth token, and hands both to Puppeteer. Puppeteer opens the Next.js report page — an unauthenticated route that accepts the token as a query param — and triggers a print.
- Unauth route with short-lived token: The report page is public but useless without the token. Token expires fast — just long enough for Puppeteer to load the page.
- data.json via SSR: getServerSideProps fetches the report JSON from the backend using the token, parses it, and passes the full typed structure to the page.
- 5 template types: Owner-wise, cloud-wise, individual owner, individual cloud, and a default framework view — same page, different render path based on TemplateType in the data.
- Ctrl+P simulation: Puppeteer calls page.pdf() which triggers the browser's print pipeline — @page margins, page-break rules, everything.
Go Backend
generates report data
JSON + Token
short-lived auth, expires fast
Puppeteer
headless Chromium instance
Next.js Page
getServerSideProps fetches + parses data
@page Print
margins, counters, page-breaks, timestamp
pixel-accurate, matches Figma layout
For the preview experience — when a user opens the report in a browser before downloading — we used @media print to make it feel like a document viewer, not a raw web page. The report header is hidden on screen and only appears in print. A4 dimensions, box shadows stripped, table rows protected from page breaks.
Two Views, One Page
- ·box-shadow on A4 cards
- ·report-header: opacity 0
- ·scrollable layout
- ·@page rules ignored
- ·box-shadow: none
- ·report-header: opacity 1
- ·A4 size, 0 margin
- ·timestamp in @bottom-right
- ·page-break-inside: avoid on rows
One detail that's easy to miss: the same page serves two different audiences. A user previewing the report in their browser sees a clean document viewer — scrollable, white background, subtle card shadow. Puppeteer generating the PDF sees the print stylesheet — no shadow, exact A4 margins, auto section counters via CSS, page numbers injected via @page bottom-right.
The generated timestamp in the PDF footer — 'Generated on Jun 12, 2024, 02:30 PM IST' — is injected as an inline <style> tag from the server with the actual time baked in. No client-side JS, no hydration gap. It's there when Puppeteer takes the snapshot.
@media print did a lot of heavy lifting here — it's not just 'hide the navbar.' It's the thing that makes the same HTML file work as both a browser preview and a pixel-accurate PDF.
Then It Hit Prod
In development and early testing, it worked well. Report generation was fast, output looked exactly like the Figma designs, and the UX of previewing before downloading was noticeably better than anything we'd shipped before.
Before this feature, the report-generation pod sat comfortably at around 250MB. After the first prod deploy — even with normal-sized reports — it jumped to 700–900MB. That was concerning, but the pod held.
- ~250MB
- 700–900MB
- ~1GB
Then we deployed to a customer environment with a large dataset. Puppeteer spun up, loaded the report page, started rendering — and the pod ran out of memory mid-generation. The process died, the pod restarted, and every metrics dashboard turned red. We reverted the build the same day.
Puppeteer isn't just a HTTP call. It's a full Chromium instance. Every concurrent report is a browser tab eating memory — and with large compliance datasets, each tab was enormous.
The fix wasn't complicated in hindsight: headless rendering at scale belongs on the client, not inside a backend pod with a 1GB ceiling. When the user's browser renders the page and triggers print, the memory cost is theirs. When the server does it, the memory cost is yours — multiplied by every concurrent request.
Where It Landed
Server-side PDF generation is off. The compliance report page still exists and still works — users can open it in a browser, preview it, and print to PDF themselves. The Figma designs, the dual-render pattern, the five template types — all of that is intact.
Server-side report generation now handles only CSV exports, the same as before. A proper fix — either a dedicated PDF microservice with tighter resource controls, or moving to a client-triggered download — is on the backlog.
It shipped, it looked great, it blew up a pod. That's a real outcome. The architecture wasn't wrong for the problem — it was wrong for the deployment context.