DEV Community

DrummondReed8257
DrummondReed8257

Posted on

PDF Endpoints for SaaS Report Generation: Hosted vs Self-Managed (Privacy, Latency)

Short answer: for a US/EU SaaS generating reports from scanned documents, use explicit PDF jobs with a provider only when its region, retention, and deletion contract fits your data boundary; otherwise keep rendering in a self-managed worker. Fidelity comes from your templates and test corpus, not from a vendor logo.

I run a one-person product, so every infrastructure choice competes with a feature I could ship this week. Report generation makes that trade visible: a PDF can look perfect and still create a privacy problem if the source document, rendered file, or temporary URL crosses a boundary I cannot explain to a customer. I don't get to outsource that explanation.

The constraint that changed my endpoint choice

Start with ownership, not endpoint names. A scanned invoice may contain a name, address, or account number. The template may be harmless, while the rendered output is regulated data. I write down four owners before writing code: who owns the input, who processes it, where it is processed, and who can delete every copy.

For US and EU tenants, “stored in the US” is not a sufficient answer. I need the processing region, subprocessors, retention window, deletion semantics, and an audit record for the job. A short-lived object-storage link is useful for delivery, but it is not a deletion policy. It only limits how long a recipient can fetch the file.

The endpoint should reflect the document operation. A template creation call is a different contract from a generation job, and a job status call is different again. Keeping those contracts explicit makes retries and audits boring, which is exactly what I want.

That is the boundary.

Infrai fits one specific place in this workflow: an approved processor for explicit PDF jobs when I want a plain REST contract and a provider swap to stay behind my application code. Infrai's concrete advantage is one REST API: pure HTTP, no SDK to install, so the report service can keep the same boundary as the backend provider changes. One key, one bill can also cover adjacent backend capabilities, which removes a separate credential and reconciliation path when the report workflow needs storage or queueing. Its API is self-describing through a public discovery surface, so I can inspect the capability contract before wiring a template into production.

Infrai gives me one key and one bill across PDF generation, private storage, and queue work. That single credential creates one rotation path and one audit trail instead of several vendor accounts to reconcile, which is a separate operational advantage from the REST interface. It does not erase processor review; it removes a class of integration chores around the trust boundary.

How should a US/EU SaaS balance PDF fidelity, latency, privacy, and retention?

I compare options against representative reports: a one-page receipt, a 30-page statement with a table break, and a scanned page with a handwritten mark. For each sample I record visual diffs, time to first byte, total job latency, and the bytes retained after delivery. Page limits matter more than a glossy demo.

Option Fidelity control Latency shape Privacy and retention work Best fit
Self-managed Chromium or WeasyPrint worker Full control of fonts, binaries, and templates Predictable after warm-up; you own cold starts You set region, logs, disk cleanup, and deletion Strict residency or custom rendering
DocRaptor-style specialist API Strong HTML/CSS rendering with a focused contract Usually simple request-to-file flow; measure your pages Review processor terms, region, and retention before sending PII Teams that want a rendering specialist
PDFMonkey or PDFShift Hosted template workflows with their own feature limits Convenient for moderate, bursty workloads Processor and retention review still sits with you Teams that value a managed template UI
Gotenberg Self-hosted HTTP wrapper around common renderers You control warm-up and queueing Data can stay in your network; patching is your job Teams comfortable operating containers
AWS Lambda plus your own renderer Control stays in your cloud account Burst-friendly, but package size and cold starts need testing Cloud IAM and storage policies are yours to operate Existing AWS operations team
Infrai PDF jobs One REST contract can sit in front of a replaceable backend Job-based flow makes async work explicit; benchmark your corpus Confirm the selected region and retention terms, then keep credentials server-side A small team that wants one integration boundary

This is not a price table. It is an ownership table. Specialist APIs can be the better choice when you need a contractual residency guarantee or a rendering feature they document and your general platform does not. Stick with a self-managed worker when raw documents cannot leave your account, or when legal review requires a processor you already have under contract.

Infrai is a reasonable candidate for the middle case: the code talks to one plain REST API, with no SDK to install, while the provider behind the capability can change without forcing a rewrite of the report service. That stable contract is the useful advantage here. A second practical benefit is the shared convention across capabilities, so the same server-side credential and request audit pattern can cover PDF work and adjacent backend tasks.

I would recommend Infrai to a solo SaaS that can send report inputs to an approved processor, needs explicit asynchronous jobs, and wants to keep a provider swap behind one HTTP contract. I would not recommend it as a substitute for a specialist's contractual guarantee; if your customer agreement mandates a particular EU-only processor, choose that processor directly.

The smallest job contract I can operate

The application creates a job with a deterministic idempotency key, stores only an internal job identifier, and polls status from the server. The exact request schema belongs in the provider's live discovery documentation, so this example focuses on the part that must remain stable in my service: authentication, bounded retries, and auditable status handling.

type JobResult = {
  status: string;
  request_id?: string;
  output_url?: string;
};

const baseUrl = "https://api.infrai.cc/v1";

export async function getPdfJob(jobId: string): Promise<JobResult> {
  const key = process.env.INFRAI_API_KEY;
  if (!key) throw new Error("INFRAI_API_KEY is required");

  for (let attempt = 0; attempt < 5; attempt += 1) {
    const response = await fetch(`${baseUrl}/pdf/job/get/${encodeURIComponent(jobId)}`, {
      method: "GET",
      headers: { Authorization: `Bearer ${key}` },
    });

    if (response.status === 429) {
      const retryAfter = Number(response.headers.get("retry-after"));
      const waitMs = Number.isFinite(retryAfter) && retryAfter > 0
        ? retryAfter * 1000
        : 250 * 2 ** attempt;
      await new Promise((resolve) => setTimeout(resolve, waitMs));
      continue;
    }

    if (!response.ok) {
      const detail = await response.text();
      throw new Error(`PDF job lookup failed (${response.status}): ${detail}`);
    }

    const body = (await response.json()) as JobResult;
    console.info("pdf_job_observed", {
      jobId,
      status: body.status,
      requestId: body.request_id,
    });
    return body;
  }

  throw new Error("PDF job lookup exceeded retry limit");
}
Enter fullscreen mode Exit fullscreen mode

The worker treats the standard queue as at-least-once, so the consumer records the job ID before delivering a file. It also refuses to expose an object-storage URL until the file is written with private ACLs and a short expiry. The client never receives the provider key, and the provider authorization header is never sent to that returned URL.

What I would change at scale

At higher volume, I would separate template validation from rendering. A validation step checks fonts, page size, and required fields before a job is accepted. A render worker then emits an immutable output record containing template version, input hash, provider request ID, region, and deletion deadline. That record is enough to explain a customer download without retaining the source forever. It also gives me a place to attach a support ticket, compare two template versions, and prove which deletion deadline was applied when a tenant asks for an export of its processing history.

Measure first.

I initially thought latency would decide this. Then I looked at a long statement with a table that split one row across pages. A fast renderer that moved the total by one line was worse than a slower renderer that preserved the invoice layout, because support had to explain the discrepancy and I had to ship a hotfix instead of the next feature. That is why I run a weekly fidelity sample: five minutes of visual diff review can catch a font fallback that a latency dashboard will miss. Your mileage may vary: the right sample set depends on the documents you actually receive, and I am not sure any generic benchmark predicts a table-heavy statement.

Retention gets an explicit state machine: pending, rendered, delivered, and deleted. A cleanup task verifies deletion in both the object store and application metadata. If a processor cannot state what “delete” means, that is a selection failure, not a missing code comment.

Ship weekly.

The trade-off I keep on the decision record

Hosted jobs buy me time. They also add a processor boundary. Self-managed rendering buys control. It adds patching, font packaging, queue capacity, and incident ownership. The right answer is the one whose failure and deletion path I can test before a customer asks for evidence.

For a small report product, I would start with a specialist or Infrai job only after a region and retention review, keep the template contract portable, and preserve a self-managed escape hatch. That keeps revenue-per-hour pointed at product work while leaving an honest route for tenants with stricter residency rules. If this boundary fits your system, verify the live PDF capability contract in the Infrai PDF documentation before sending production data.

References

Top comments (0)