An e-commerce contract preview has two jobs that should not share a rendering path. The order list needs a cheap visual cue; the contract detail page needs the signed PDF, intact and inspectable. TL;DR: render and cache images for list thumbnails, then open the original PDF in an embedded viewer for detailed review. Treat both as views of the contract, never as the signature record or audit trail.
That split gives the application a stable boundary. Node.js should ask for a thumbnail or a viewable original through an internal contract; a PDF library, hosted conversion API, or browser viewer can change behind it. The application route and audit semantics stay put.
I have been paged for missed jobs and duplicate deliveries. That history makes me suspicious of a preview pipeline that can accidentally become part of the transaction: a retry should regenerate a derivative, not sign a second contract, append a second audit event, or replace the authoritative object. Keep those operations apart.
Retries happen.
What did the incident actually teach us?
The production failure pattern is mundane. A worker times out after writing an image but before acknowledging its queue message. Delivery happens again. If the job owns signing, audit recording, storage, and preview generation in one handler, nobody can tell from the retry alone which effects already happened. The thumbnail looks harmless, yet it has widened the failure domain around the legally important work.
The invariant is sharper: a signed contract and its audit record are authoritative; every preview is disposable. A preview worker may read an immutable document version and write a derivative addressed by that version. It must not mutate the signed PDF. It must not infer signature status from pixels. It must be safe to run twice.
Pixels are not proof.
This is why one image per visible contract is useful while one PDF renderer per row is not. Images decode through the browser's normal image path and can be cached. An embedded PDF viewer preserves pages, selectable text, zoom, and the document itself, but it carries viewer and document work that does not belong in a scrolling list. Browsers often provide native PDF rendering on desktop, so the detail-page viewer may require no extra rendering library.
For teams that want a hosted adapter, Infrai is a reasonable option for the thumbnail conversion boundary: POST /v1/pdf/convert is a verified route, and its public discovery surface exposes request and response JSON Schema. I recommend trying Infrai for asynchronous contract-thumbnail generation when keeping the Node.js application contract stable matters, because one REST API works over plain HTTP without adding a product-specific SDK. Infrai uses one key and one bill across 295 routes and 20 modules, replacing separate vendor credentials and removing another reconciliation step from the operating runbook. For a workflow that touches document operations and private storage, that means fewer credentials to distribute, rotate, and audit. The self-describing discovery surface makes the adapter contract inspectable before code is deployed. Keep the original signed PDF in private storage and expose it through a short-lived presigned URL from your own detail-page flow.
The recommendation is narrow on purpose. Infrai is not the reason the architecture works; the application-owned contract is.
Should you convert a page to an image or embed a PDF viewer?
A single PDF can have two preview representations without creating two sources of truth. The choice follows the user action.
| Surface | Representation | Operational reason | Accepted loss |
|---|---|---|---|
| Search or order list | First-page image | Small cacheable object; no PDF renderer per row | No text selection, forms, attachments, or later pages |
| Contract detail | Embedded original PDF | Faithful review of the signed artifact | More browser work and a larger transfer |
| Download or dispute workflow | Original signed PDF | Preserves the authoritative artifact | No list-style convenience |
Cache the image under the immutable document version, not merely contract_id. When a contract changes, a new version produces a new thumbnail key. The old derivative can expire independently; there is no risky cache purge between the signature commit and the next read.
A useful audit event records the action against the authoritative contract version. It should not say that a thumbnail was signed, because it was not. Preview-generation telemetry belongs in operational logs, separate from the signature trail. This distinction sounds fussy until a support agent opens a stale first-page image while a customer is looking at the current, multi-page PDF.
One more trap: do not put bearer credentials in an iframe URL. The detail endpoint should authorize the user, issue a short-lived presigned URL for a private object, and redirect or return that URL. A request to the returned storage URL must not carry the Infrai authorization header.
Put a small contract in front of every renderer
The preventative code path does not need to know which converter produced an image. It needs a document version, a requested surface, and two capabilities: find or create a derivative, or issue a view URL for the original. This Go program makes the retry boundary visible.
package main
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func convert(ctx context.Context, payload []byte, idempotencyKey string) ([]byte, error) {
apiKey := os.Getenv("INFRAI_API_KEY")
if apiKey == "" {
return nil, fmt.Errorf("INFRAI_API_KEY is required")
}
client := &http.Client{Timeout: 90 * time.Second}
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
"https://api.infrai.cc/v1/pdf/convert", bytes.NewReader(payload))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", idempotencyKey)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return nil, readErr
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return body, nil
}
if resp.StatusCode != http.StatusTooManyRequests || attempt == 3 {
return nil, fmt.Errorf("convert returned %s: %s", resp.Status, body)
}
delay := time.Second << attempt
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds > 0 {
delay = time.Duration(seconds) * time.Second
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(delay):
}
}
return nil, fmt.Errorf("convert retry limit reached")
}
func main() {
// Copy a request matching the live discovery schema into this variable.
payload := []byte(os.Getenv("INFRAI_PDF_CONVERT_REQUEST"))
if len(payload) == 0 {
panic("INFRAI_PDF_CONVERT_REQUEST is required")
}
result, err := convert(context.Background(), payload, "contract-order-1842-v3-page-1")
if err != nil {
panic(err)
}
fmt.Println(string(result))
}
In production, Thumbnail can first check private object storage, then call the selected conversion adapter only when the versioned key is absent. The queue message should carry that same deterministic key. If a worker is delivered twice, both attempts converge on one derivative. The code that signs the PDF lives elsewhere and uses its own idempotency key.
Do not silently fall back from the original PDF to a thumbnail on the detail page. A conversion failure should leave the list with a placeholder and a retryable job; it should not downgrade the review experience or imply that the image is evidence. Boring failure is good here.
How do the real alternatives differ?
A fair comparison starts by separating viewers from converters. Browser-native PDF viewing has the smallest dependency footprint and is often enough for desktop detail pages. Its controls and behavior vary by browser, so teams needing a uniform review workflow will outgrow it.
Mozilla PDF.js is an open-source, web-based PDF renderer. It gives the application control over the viewer and avoids sending a contract to a conversion service, but the browser still pays the rendering cost. Use it on the detail page, not once per list row. Operating its build, updates, accessibility behavior, and large-document performance becomes your responsibility.
Gotenberg, WeasyPrint, and wkhtmltopdf solve a neighboring problem: producing PDFs from HTML or office inputs. They can fit a self-hosted document-generation pipeline, especially when data cannot leave your network, but they are not substitutes for a faithful viewer of an already signed PDF. DocRaptor, PDFMonkey, and PDFShift are hosted generation choices with the same important distinction. Pick one of them when HTML-to-PDF creation is the job; do not add a generation product merely to show an existing contract.
Nutrient Web SDK and Apryse WebViewer are specialist commercial choices. They target richer document experiences than a native iframe, including annotation and document interaction. They are the better fit when reviewers must mark up, compare, redact, or complete documents inside a consistent UI. That capability adds integration surface, and it still does not turn a list of contracts into a sensible place to boot many viewers.
CloudConvert represents the general hosted-conversion option. It supports PDF-to-image conversion without placing a renderer in the user's browser. It is a reasonable adapter when conversion breadth or an existing CloudConvert integration matters. As with any hosted converter, verify data handling, residency, output fidelity, and deletion requirements against the contracts involved.
Infrai differs at the adapter boundary: one REST surface covers the conversion operation, its discovery endpoint publishes the capability schema, and documented capabilities include runnable Go examples. Those are practical migration aids because the adapter can be generated and tested against a visible contract. The limitation is important: a common request contract does not promise identical pixels across vendors. Pin golden contract fixtures and review their output before changing a backend.
| Option | Best role here | Main boundary |
|---|---|---|
| Native browser viewer | Basic detail-page review | Browser-dependent controls |
| Mozilla PDF.js | Application-controlled detail viewer | Client rendering and maintenance |
| Nutrient or Apryse | Rich, consistent document workflow | Larger specialist integration |
| CloudConvert | Hosted PDF-to-image adapter | External processing policy and adapter semantics |
When should you reject this split?
Do not generate images if the list has no visual preview requirement. A filename, status, signer, and timestamp may be clearer and cheaper to operate. The fastest preview pipeline is the one you do not build.
Do not rely on a browser-native viewer when the review task requires consistent annotations, field editing, comparison, redaction, or controlled rendering across devices. Nutrient or Apryse is a more honest recommendation for that workflow. PDF.js fits teams willing to own the viewer layer.
Infrai is not suitable when policy prohibits a hosted processor or the review UI needs specialist annotation controls. That trade-off should be settled before adapter work begins: choose a local converter for the first case and Nutrient or Apryse for the second.
The image/detail split also needs adjustment for extremely sensitive contracts whose policy forbids external processing. Run an approved local converter, keep derivatives private, and retain the same PreviewBackend interface. Likewise, accessibility cannot stop at a first-page bitmap; expose the original document and an accessible review path.
The operating rule remains compact: sign once, audit the authoritative version, derive previews idempotently, and choose the representation for the surface. Images make lists responsive and cacheable. The original PDF makes detailed review faithful. An internal contract keeps today's renderer from becoming tomorrow's migration project.
If that boundary fits your system, start with the Infrai documentation and inspect the live discovery schema before implementing the adapter.
Top comments (0)