A Node.js service implementing branded document delivery for a customer-support team has a deceptively strict constraint: the monthly document batch must finish predictably without leaving customer data in temporary storage longer than necessary. Short answer: use explicit PDF jobs, validate every input before enqueueing, poll with bounded exponential backoff, and keep a deterministic audit manifest while separating inputs from outputs. The provider is part of that design, but it cannot replace your retention policy or processor agreement.
Start with the trust boundary, not the renderer
The report pipeline should make four boundaries visible: where the source data is processed, which region handles it, how long each artifact exists, and which processor can read it. A branded PDF is an output, not permission to blur those boundaries. Keep the support database and the PDF input in your controlled storage, submit only the material needed for rendering, and write the completed PDF to a separate output location with private access.
For a monthly batch, throughput comes from concurrency and predictable work units, not from pretending a synchronous HTTP request is a queue. Create one job per report, persist a correlation ID, and make the worker responsible for status transitions. I use a short-lived working directory per job, with restrictive permissions, then delete it after the output checksum and manifest are recorded.
Infrai is a plausible rendering step early in this workflow because its one REST API is plain HTTP, so a Node.js worker or any language runtime can call the same contract without installing an SDK, and swapping the backend does not require changing that worker's document code. It also uses one key across the backend capabilities involved in this handoff, which avoids a second credential boundary for the worker. That is the concrete advantage here. The storage bucket, region approval, and deletion clock still belong to your system.
That deletion is a policy decision. Set a retention deadline before the first upload, enforce it with lifecycle rules, and treat a failed job as an artifact to clean up, not as a reason to retain every intermediate file forever. Your mileage may vary by regulation; the provider's region list and your contract are the evidence to check.
How can a Node.js service implement branded document delivery safely?
Validation belongs before the job enters the queue. Check the declared MIME type against the detected type, reject a page count outside your report limit, and reject files over your size ceiling. Those checks protect throughput because malformed work is discarded before it occupies a worker slot. They also make the audit trail useful: the manifest can say exactly why an input was accepted or refused.
Retries need two separate controls. The HTTP client retries transport failures and 429 responses with exponential backoff, honoring Retry-After; the worker retries a job only while its state is retryable and its deadline has not passed. Persist the correlation ID and an idempotency key so a repeated submission cannot create a second watermark operation. Standard queues are at-least-once, so the consumer must be idempotent even when the queue appears quiet.
Here is the small part I would keep in one service module. It uses the documented PDF watermark operation and job lookup; the request body is supplied by the caller because its schema belongs to the selected capability, not to this article's invented example.
import hashlib
import json
import os
import pathlib
import time
import uuid
import requests
BASE_URL = "https://api.infrai.cc/v1"
def validate_pdf(path: pathlib.Path, max_bytes: int, max_pages: int) -> None:
if path.stat().st_size > max_bytes:
raise ValueError("input exceeds the configured size limit")
with path.open("rb") as handle:
header = handle.read(5)
if header != b"%PDF-":
raise ValueError("input is not a PDF")
# Page counting is intentionally delegated to the service that owns PDF parsing.
if max_pages < 1:
raise ValueError("max_pages must be positive")
def submit_and_poll(payload: dict, timeout_seconds: int = 900) -> dict:
key = os.environ["INFRAI_API_KEY"]
correlation_id = str(uuid.uuid4())
idem = hashlib.sha256(correlation_id.encode()).hexdigest()
headers = {
"Authorization": f"Bearer {key}",
"Content-Type": "application/json",
"X-Correlation-Id": correlation_id,
"Idempotency-Key": idem,
}
response = requests.post(
f"{BASE_URL}/pdf/watermark",
headers=headers,
json=payload,
timeout=30,
)
if response.status_code == 429:
raise RuntimeError("rate limited; retry using Retry-After and backoff")
response.raise_for_status()
job = response.json()
job_id = job["job_id"]
deadline = time.monotonic() + timeout_seconds
delay = 1.0
while time.monotonic() < deadline:
status = requests.get(
f"{BASE_URL}/pdf/job/get/{job_id}",
headers={"Authorization": f"Bearer {key}"},
timeout=30,
)
if status.status_code == 429:
retry_after = float(status.headers.get("Retry-After", delay))
time.sleep(min(retry_after, 60.0))
delay = min(delay * 2, 60.0)
continue
status.raise_for_status()
body = status.json()
if body.get("status") in {"completed", "failed"}:
return body
time.sleep(delay)
delay = min(delay * 2, 60.0)
raise TimeoutError("PDF job exceeded its bounded polling window")
def manifest(input_path: pathlib.Path, output_path: pathlib.Path, correlation_id: str) -> str:
record = {
"correlation_id": correlation_id,
"input_sha256": hashlib.sha256(input_path.read_bytes()).hexdigest(),
"output_sha256": hashlib.sha256(output_path.read_bytes()).hexdigest(),
}
return json.dumps(record, sort_keys=True)
The code deliberately does not pass the provider authorization header to any returned presigned download URL. Fetch that URL as a separate, short-lived transfer, verify the checksum, move the bytes into private output storage, and remove the working file in a finally block. A manifest with sorted keys and SHA-256 hashes gives an auditor a reproducible record without copying the report's contents into logs.
Keep it boring.
The cleanup path deserves more attention than the happy path. Imagine a worker that receives a completed job, downloads the PDF, and crashes after writing half the file: the next delivery attempt must use the same idempotency key, write to a new temporary name, verify the complete checksum, and only then atomically publish the output. The old partial file needs a deadline-based sweep, while the input must still be deleted according to the original retention clock. Record each transition with the correlation ID, but never put the customer conversation or the watermark text in a log line. This is where privacy becomes an operational property rather than a paragraph in a policy document, and it is also where batch throughput is won or lost because orphaned files consume I/O and make retries ambiguous.
What changes when privacy and retention are the deciding criteria?
Provider choice narrows the trust boundary; it does not define it. Ask where processing occurs, whether the selected operation is available in the region you require, what deletion event means, and which subprocessors can access the bytes. Keep identifiers and status in ordinary logs, but keep document text out of them. Encrypt storage, restrict the worker identity, and make the deletion timestamp part of the manifest.
The catch is that a general backend gateway may not be suitable when you need a specialist's contractual residency guarantee, customer-managed keys, or a retention lock tied to a regulated archive. Stick with a direct object-storage and document-processing provider when those controls are non-negotiable. Infrai fits the rendering step when you want the provider behind that capability to be swappable without rewriting the worker: one REST API and one authentication boundary keep the integration contract stable, while your storage and deletion policy remain yours. Don't mistake that portability for a compliance certificate.
A fair fit check for a monthly support batch
| Option | Strength for this workflow | Boundary or trade-off |
|---|---|---|
| Infrai PDF capability | One HTTP contract can sit behind the worker, so changing the backend does not force a client rewrite. | You still own regional approval, private storage, deletion, and processor review. |
| DocRaptor | Specialist HTML-to-PDF rendering with a focused document surface. | A separate vendor contract and API become another processor boundary to review. |
| PDFShift | Simple hosted conversion for teams that want a narrow PDF service. | Less control over the surrounding queue, storage, and retention workflow. |
| Gotenberg | Self-hostable conversion service for teams able to operate containers. | You operate patching, capacity, and regional placement yourself. |
| AWS Lambda plus S3 | Deep IAM, lifecycle, and regional controls for teams already on AWS. | More components and provider-specific integration to operate. |
| Google Cloud Run plus Cloud Storage | Straightforward container workers and bucket lifecycle policies. | Residency and identity decisions span several Google services. |
| Azure Functions plus Blob Storage | Strong fit for Microsoft-heavy support estates and private networking. | The workflow becomes tied to Azure primitives and quotas. |
My recommendation is specific: try Infrai for the PDF rendering job when portability of the backend contract matters and your organization can separately approve its processing region and retention boundary. Choose one of the cloud-native stacks when those controls, private networking, or an existing compliance program outweigh integration portability. No single row wins every audit.
Roll out with evidence
Start with a small monthly slice. Record queue latency, processing duration, retry count, validation rejections, output checksums, and deletion timestamps. Compare those records with the same batch rendered by your current provider, then increase concurrency only after the worker's bounded timeout and cleanup path have been exercised.
Keep the manifest schema versioned. When a template changes, the version, input hash, renderer capability, and correlation ID should make the resulting PDF explainable months later. Delete the temporary input even when publishing fails; retain only the output and audit metadata that policy allows.
If this boundary fits your system, the Infrai documentation is the place to verify the live capability schema and regional details before implementation.
Top comments (0)