Get a quote

Generating PDF Documents in a Go SaaS Backend: Invoices, Statements, and Reports

Every SaaS product eventually needs to generate PDF documents. Invoices, monthly statements, delivery receipts, and payroll reports are all formats that clients expect as downloadable PDFs. This is how we approach PDF generation in Go backends for SaaS products serving clients across Lebanon and the MENA region.

Every SaaS product eventually needs to generate PDF documents. Invoices, monthly statements, delivery receipts, and payroll reports are formats that clients expect as downloadable PDFs. This is how we approach PDF generation in Go backends for SaaS products serving clients across Lebanon and the MENA region.

Why PDF generation in Go is harder than it looks

Go has no native PDF rendering library that handles complex layouts well. The two approaches that work in production are: using html/template to produce an HTML document and rendering it to PDF with a headless browser, or using a Go-native PDF library that builds documents programmatically.

The headless browser approach (via a containerized Gotenberg or Chromium instance) produces the highest-quality output because you write the layout in CSS and HTML, which every developer on the team already knows. The programmatic approach using libraries like go-pdf or gofpdf gives you more control over file size and avoids the operational complexity of running a headless browser, but requires you to build your layout in code rather than markup.

For most SaaS products, the headless rendering approach wins on output quality and developer velocity. The operational overhead of running one Gotenberg container is low.

The HTML template plus Gotenberg approach

Gotenberg is a Docker-based HTTP API that wraps Chromium. You send it an HTML file and it returns a PDF. In Go, the flow looks like this:

  1. Render the invoice template to an HTML string using html/template
  2. Send a multipart HTTP request to the Gotenberg API with the rendered HTML
  3. Receive the PDF bytes in the response body
  4. Write the PDF to S3 or return it directly to the client

The template is standard Go HTML templating. You can use any CSS you want, load web fonts, include tables, and control page layout with @page CSS rules. Gotenberg supports headers and footers, multi-page documents, and landscape orientation.

One important detail for MENA clients: the HTML template should support right-to-left text rendering for Arabic invoices. Adding dir="rtl" and lang="ar" to the HTML root element, with an Arabic font loaded via a web font link, produces correct Arabic PDF output through Gotenberg. This matters for clients who need bilingual invoices with both Arabic and English sections on the same page.

Structuring the template data model

For an invoice PDF, the Go struct that feeds the template should carry everything the template needs:

type InvoiceData struct {
    InvoiceNumber string
    IssuedDate    time.Time
    DueDate       time.Time
    Company       CompanyInfo
    Client        ClientInfo
    LineItems     []LineItem
    Subtotal      decimal.Decimal
    TaxRate       decimal.Decimal
    TaxAmount     decimal.Decimal
    Total         decimal.Decimal
    Currency      string
    Notes         string
    Locale        string // "en" or "ar"
}

The Locale field controls which HTML template file gets loaded. We keep separate template files for English and Arabic invoices rather than trying to handle both in one template with conditional blocks. The templates share the same CSS file but the HTML structure, text direction, and column ordering differ enough between English and Arabic that separate templates are cleaner in practice.

Handling decimal precision for MENA tax requirements

Saudi Arabia, the UAE, and Lebanon all have specific VAT and tax display requirements on invoices. Saudi Arabia requires the invoice to show the VAT amount to two decimal places in SAR. The UAE requires the same for AED. Lebanon has its own value-added tax regulations that vary by sector.

Using Go's shopspring/decimal package for all monetary values in the PDF generation pipeline prevents floating-point rounding errors from appearing on client invoices. A total that rounds to 1000.00 SAR on the screen should not show as 999.9999998 on the PDF because of float64 arithmetic. The decimal.Decimal type eliminates this class of bug entirely.

Template functions handle formatting: a custom FormatMoney function takes a decimal.Decimal and a currency code and returns the correctly formatted string for that currency, including the appropriate number of decimal places and the correct symbol position (prefix or suffix depending on the locale).

Caching and storage: when to generate on the fly versus on demand

For invoices that are generated once and rarely changed, the pattern that works well in production is: generate the PDF at invoice creation time, store it on S3 (or Cloudflare R2), and serve the stored file on subsequent downloads. This avoids re-rendering the PDF on every download request, keeps Gotenberg load predictable, and gives you a permanent record of exactly what the client received.

For reports that aggregate live data (a daily sales report, a monthly usage statement), generate on demand with a short TTL cache. Store the generated PDF in S3 with a key that includes the report date. If a second request for the same report arrives within the TTL window, return the cached version. After the TTL expires, regenerate from current data.

For large batch report runs (generating 500 invoices at month-end billing time), push the generation jobs into a background queue and process them with a worker pool. The worker pool can limit concurrency to avoid overloading the Gotenberg container. Each generated PDF is stored on S3 immediately and a download URL is sent to the client via email or webhook when the batch completes.

Handling font loading in a containerized environment

Gotenberg renders using Chromium, which relies on system fonts and web fonts loaded via HTTP. In a containerized deployment, system fonts may be limited. The safest approach is to load all fonts via web font links in the template HTML. Google Fonts URLs work from within the Gotenberg container as long as it has outbound internet access.

For environments with no outbound internet access (some Gulf region deployments), serve the font files from your own S3 bucket with public read access and reference them in the template with absolute URLs. This gives you predictable font loading without depending on Google's availability.

Watermarking draft documents

Many SaaS products need to watermark PDF documents that are in draft or unpaid status. The simplest implementation in Go with the Gotenberg approach is to add a watermark layer via CSS: a fixed-position element with position: fixed; top: 0; left: 0; width: 100%; height: 100%; opacity: 0.08; z-index: 9999; containing the watermark text rotated 45 degrees. This CSS watermark renders cleanly in Chromium-based PDF output and is visible without being distracting.

For watermarks that must be tamper-resistant (legal documents, formal statements), add the watermark at the PDF binary level using a library like pdfcpu after receiving the rendered PDF from Gotenberg. This adds a second processing step but produces a watermark that cannot be removed by editing the HTML template.

Key lessons from production

Gotenberg solves the hard layout problems. CSS flexbox and grid work in Chromium-based PDF rendering. You get headers and footers, page numbers, and page breaks for free. The HTML approach scales naturally as document designs become more complex.

Separate templates for each locale. Mixing English and Arabic layout logic in a single template file creates unmaintainable conditionals. Two clean templates are easier to update than one complex one.

Store generated PDFs at creation time for transactional documents. Re-generating invoices on every download wastes compute and creates a risk of the document looking different on the second download if the template has changed.

Use shopspring/decimal for all monetary values. Float64 arithmetic is not appropriate for tax calculations on client-facing documents.

Test your Arabic rendering end-to-end. Right-to-left text in PDFs has specific rendering quirks that only appear in the final output. A test that generates an Arabic invoice and checks the page dimensions is not enough. You need to visually verify the Arabic text alignment, number formatting, and column ordering in the actual PDF file.

Free PDF Download

Enjoying this article?

Enter your email and get a clean, formatted PDF of this article - free, no spam.

Free. No spam. Unsubscribe any time.

Not sure where to start?

Voxire builds production Go backends for SaaS products across Lebanon and the MENA region, including document generation pipelines for invoices, reports, and statements in English and Arabic. If you are building a SaaS product and need reliable PDF generation, we can help design the architecture.

https://voxire.com/get-a-quote/

Back to blog
Chat on WhatsApp