Compliance Reporting Platform
The report looked like the product. Then real customer data showed that rendering architecture and execution architecture were two different decisions.
- Frontend Engineer
- Compliance Engineering
- 2025
- Customer-facing compliance reports had to look like the product (branded, paginated, previewable), and the backend was primarily Go.
- Compose the document in the existing Next.js frontend and use Chromium's print pipeline as the renderer, behind a short-lived token on an SSR route.
- The document architecture solved the problem; the deployment model did not contain the workload. Automated generation was rolled back and never re-enabled.
The PDF Looked Like a Product
The hard part was never writing bytes into a .pdf file. Aurva needed customer-facing compliance reports with branded title pages, status summaries, severity badges, long violation tables, framework breakdowns, appendices, a table of contents, timestamps, and page numbers. Customers also needed to preview the document before exporting it.
This was greenfield work. The backend was primarily Go, so the first architectural decision was whether the document should become a backend-owned template system or a real frontend surface rendered through the browser.
The question was not “Which PDF library?” It was “Where should the report's presentation system live?”
Let the Frontend Render It Once
I proposed building the report in the existing Next.js frontend and using Chromium's print pipeline as the document renderer. Aurva already had typography, color tokens, tables, badges, icons, and spacing conventions there. Rebuilding those primitives in Go would have created a second presentation system that could drift every time the product design changed.
Reuse the visual language
Compose reports from the frontend design system instead of approximating the Figma work in a separate backend template stack.
Keep the preview real
Make the rendering URL a useful browser document with scrolling and anchor navigation, not an invisible intermediate artifact.
Print the same semantic tree
Use @media print and paged-media CSS to turn that HTML into an A4 document rather than maintaining a second PDF-only composition.
The result was template-driven, component-based report composition, not a universal schema renderer. That boundary kept the system concrete enough to match the approved layouts while still sharing the parts that were actually common.
SSR as Rendering Infrastructure
The backend created a very short-lived token and opened a special route shaped like /reports/compliance?reportId=…&token=…. The route intentionally did not depend on a normal logged-in browser session. getServerSideProps read the parameters, fetched the serialized report payload with the token, parsed it, and rendered the document on the server.
SSR mattered because the page's machine consumer was a headless browser. Chromium received substantially rendered HTML instead of booting a client shell, fetching data, waiting for React state, waiting for layout, and only then entering the print pipeline.
At the time, Aurva did not yet have RBAC. The historical token was generic and short-lived, not narrowly scoped to one report permission. That was a pragmatic control for the original system, but it is not the authorization model I would design today.
Short-lived is not the same as least-privilege. The route reduced exposure time; it did not make the token report-scoped.
One Domain, Five Report Projections
The shared outer document rendered the report title, header, table of contents, page rules, and common layouts. TemplateType then selected the major summary, compliance, and appendix sections for DEFAULT, OWNER_WISE, INDIVIDUAL_OWNER_WISE, CLOUD_WISE, or INDIVIDUAL_CLOUD_WISE reports.
Synthetic compliance report
One dataset, three projections
Overall
82%
21 open findings
TemplateType · DEFAULT
The audit grouped by the policies a reviewer needs to sign off.
- 91%Access controls3 open
- 84%Data protection7 open
- 72%Audit readiness11 open
The production reports shared a document shell and compliance domain data, then selected a composition from TemplateType. This synthetic example keeps the overall result fixed while changing how the same findings are grouped for a framework reviewer, cloud owner, or accountable team.
const projection = {
[REPORT_TYPES.DEFAULT]: FrameworkReport,
[REPORT_TYPES.CLOUD_WISE]: CloudReport,
[REPORT_TYPES.OWNER_WISE]: OwnerReport,
} satisfies Record<TemplateType, ReportComponent>;
function ComplianceReport({ report }: Props) {
const ReportSections = projection[report.data.TemplateType];
return (
<ReportDocument>
<ReportTitle data={report.data} />
<TableOfContents data={report.data} />
<ReportSections report={report} />
</ReportDocument>
);
}- Framework: Compliance overview, framework sections, and a unified violation appendix.
- Cloud: Overall and cloud summaries, per-cloud compliance, and cloud-wise appendices.
- Owner: Overall and user summaries, per-owner compliance, and owner-wise violation details.
The table of contents was built independently from the rendered document, although both consumed the same TemplateType and ordered data. TOC numbers came from array indices; body headings mixed CSS counters with manually rendered numbers. Anchors such as cloud-compliance-aws kept the browser document navigable, but there was no single document tree preventing numbering or structure from drifting.
Two Render Modes, One HTML Tree
- ·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
On screen, the report behaved like a document viewer: scrollable white pages, subtle shadows, and anchor-based TOC navigation. In print, @media print removed the screen treatment, enabled an A4-aware layout, revealed the fixed header, and activated page-break rules.
- Logical units: break-inside: avoid kept status cards and appendix records from splitting awkwardly.
- Long tables: Rows resisted page breaks while thead { display: table-header-group } repeated context on each page.
- Semantic boundaries: break-before: page started owner, cloud, and appendix sections cleanly; the TOC used break-after: page.
- Generated furniture: @page margin boxes carried an SSR-baked timestamp and counter(page), with internal space reserved for the fixed header and footer.
Print CSS was not a cleanup pass. It was the document layout engine layered over the same semantic report.
A Report Was a Job, Not a Request
Generation was asynchronous. A user configured report scope, frameworks, filters, and optional email or Slack delivery, then returned to the product while the job moved through Generating, Completed, or Failed. Backend-owned retries and a manual Retry action handled failures without turning the create flow into a blocking screen.
Async report job
Generating · Completed · Failed
Go backend
prepares data + short-lived token
Go + chromedp
drives headless Chromium
Next.js SSR page
getServerSideProps fetches + renders
PrintToPDF
A4 CSS, page breaks, counters, headers
Stored PDF
report list + optional email / Slack
The frontend owned the rendering architecture, SSR route, report components, variants, and print behavior. The backend team owned report data, token issuance, job orchestration, Go + chromedp execution, PDF storage, retry lifecycle, and external delivery. The end-to-end feature was a collaboration, even though the browser-rendering direction was my proposal.
The Document Worked. Its Placement Did Not.
The reports matched the intended designs in development and smaller workloads. In production, however, the chromedp/Chromium path substantially increased memory pressure in the existing report-generation pod. A large customer workload eventually crossed its memory ceiling, the pod restarted, and automated PDF generation was rolled back.
- ~250MB
- ~700–900MB
- ~1GB
Those memory figures are historical recollections, not current monitoring evidence. The exact incident mix was never isolated: one exceptionally large report, concurrent renders, or both may have contributed. Browser reuse reduced startup overhead, but reuse alone did not provide resource backpressure.
The lesson is not “Chromium can never generate PDFs.” It is that a browser renderer is a resource-intensive workload that needs an execution architecture with explicit capacity management.
The Report Page Survived
Automated server-side PDF generation was not re-enabled. The surviving production feature is the Next.js compliance-report page: five report variants, SSR data loading, shared sections, the TOC and anchors, A4-aware print styles, table pagination, headers, footers, and manual browser Print to PDF.
That outcome matters because it separates two decisions that are easy to collapse. The document architecture solved the presentation and maintainability problem. The deployment model failed to safely contain the rendering workload.
It shipped, produced the right output, and exposed a production assumption that did not survive customer scale. The useful part stayed; the unsafe automation did not.
What I Would Change Today
Scope the rendering credential
Use today's RBAC foundation to issue a very short-lived, least-privilege token limited to one report resource and the read operation required for rendering.
Create one document tree
Derive section order, anchors, TOC entries, numbering, and rendered content from the same typed structure instead of coordinating parallel systems by convention.
Bound the renderer
Move Chromium into dedicated workers with a queue, strict concurrency, timeouts, size limits, resource isolation, lifecycle monitoring, and capacity-aware autoscaling.
Benchmark Typst, do not assume it
Test design fidelity, complex tables, all five projections, render speed, memory, and preview requirements before treating a document-native renderer as the rewrite answer.
The async UX already made bounded queueing compatible with the product: users were expecting Generating, Completed, and Failed states rather than an immediate download. The missing work was operational backpressure, not a new interaction model.
Rendering architecture decides how a document is expressed. Execution architecture decides whether producing it is safe. This project taught me to design both explicitly.