DEV Community

YorkHolloway3257
YorkHolloway3257

Posted on

Node.js PDF Preview API: When to Embed, Convert, or Own the Viewer

TL;DR: For a marketplace invoice that already exists as PDF, serve the exact signed bytes from an authenticated Node.js endpoint and embed that URL in the browser's native viewer. Do not convert the invoice to images or HTML merely to preview it. Conversion creates a second representation that can disagree with the signed artifact; use it only when the product requirement is a thumbnail, text extraction, redaction, or a controlled fallback for clients that cannot display PDF. The page worth firing is not "preview failed." It is "buyers cannot retrieve the immutable invoice whose digest is recorded in the audit trail."

The on-call view should be painfully specific: invoice inv_01J8M4, order ord_84271, artifact revision 3, an HTTP 5xx rate above the service objective for the signed-byte route, and a trace that ends at object retrieval. A dashboard full of PDF render duration, CPU, and queue depth may look sophisticated while missing the event that matters: the customer clicked an invoice and did not receive the bytes named by the ledger.

That distinction decides the architecture.

A preview is a delivery path for evidence, not a new document-generation job.

What should the browser preview endpoint actually return?

Return one immutable PDF representation with Content-Type: application/pdf, a deliberate Content-Disposition, validators, byte-range support where the storage path can honor it, and authorization performed before any bytes leave the service. RFC 9110 defines representation metadata, conditional requests, range requests, and status-code semantics; the PDF media type is registered as application/pdf. An inline disposition asks the user agent to display content rather than download it, but it does not promise that every browser has a PDF renderer.

For an invoice, the identifier in the UI should resolve to an artifact record, not to a mutable object-store key invented in the request handler. That record needs the order ID, invoice revision, object location, byte length, media type, cryptographic digest, signature state, generation timestamp, and the actor or process that finalized it. Keep the digest over the stored PDF bytes. If a signer later produces a new byte sequence, that is a new revision with its own digest, even when the pages look identical.

Here is the shape of the boundary in Go. The surrounding application may be Node.js, but the contract is language-independent, and keeping it small makes the operational promise easier to inspect.

type InvoiceArtifact struct {
    InvoiceID   string
    OrderID     string
    Revision    int
    ObjectKey   string
    MediaType   string
    ByteLength  int64
    SHA256      string
    FinalizedAt time.Time
}

type ArtifactStore interface {
    Open(ctx context.Context, key string, start, end int64) (io.ReadCloser, error)
}

type AuditSink interface {
    RecordRead(ctx context.Context, invoiceID, revision, actorID, traceID string) error
}
Enter fullscreen mode Exit fullscreen mode

The browser page can use an <iframe> or <object> whose source is that endpoint. Keep authorization on the request path. A short-lived, scoped URL is another valid boundary when the object service delivers bytes directly, but do not put durable bearer credentials in markup, logs, analytics events, or referrer-bearing URLs. The audit record should say which authenticated actor requested which immutable revision and whether delivery started; it should not claim the human read every page.

There is one subtle trap here. Logging a successful application authorization and then returning a redirect is not the same as observing successful artifact delivery. If direct object delivery matters to the service objective, correlate the application trace with storage access telemetry or a controlled delivery proxy. Otherwise the green line ends one hop too early.

The redirect lies.

Work backward from the page

Suppose the page fires on a burst of 500 responses from the invoice-byte endpoint. The runbook first separates four outcomes because they demand different action: authorization rejection, artifact not found, upstream read failure, and client cancellation. Combining them into "preview error" guarantees a noisy pager. A 403 caused by a buyer requesting another seller's invoice is a security-relevant denial, not an availability failure; an application 404 for an artifact that the ledger says is finalized is an integrity failure; a client closing the tab halfway through a range response is usually neither. Work back one step. Before customers fail, the useful signal is a mismatch between finalized artifact records and retrievable objects, measured by a low-rate synthetic check that selects a dedicated canary invoice, verifies the returned media type and byte count, and hashes the complete response against the recorded digest. The canary must never contain customer data. A header-only probe is cheap but weak: it can pass while the object body is truncated or replaced. The tempting assumption is that a successful metadata response proves the invoice is healthy; the body check is what disproves it.

Earlier still, generation should refuse to mark an invoice final until the bytes have been durably written, read back as required by the storage consistency model, hashed, and bound to the artifact revision. Signature validation belongs at that transition and in an asynchronous audit, not on every interactive preview request. Revalidating a document signature on every page view increases latency and turns certificate or validation dependencies into a read-path outage. The read path can compare stable metadata and stream the already-finalized bytes.

The instrumentation change is therefore modest. Add counters partitioned by outcome rather than raw exception class, a histogram for time to first byte, a histogram for completed bytes relative to expected bytes, and trace attributes for artifact revision and delivery mode. Do not attach order payloads, buyer details, full signed URLs, or invoice contents. Cardinality matters too: invoice IDs belong in sampled traces and audit records, not metric labels.

I distrust a dashboard that cannot answer one blunt question: what page fired? A useful alert names the violated customer outcome and links to traces for failed deliveries. Queue depth for invoice generation can be an early warning, yet it should page only when it predicts missed finalization objectives; otherwise it is a ticket for daylight hours.

Convert, embed, or render your own viewer?

The decision is less about visual polish than about which representation the business treats as evidence.

Approach Bytes shown to the buyer Signature and audit consequence Operational cost Use it when
Embed the PDF response The finalized PDF itself Digest and signature stay tied to the viewed artifact Browser capability and range behavior vary The invoice PDF is the authoritative record
Convert pages to images Derived raster files Images are not the signed PDF and need explicit lineage Rendering workers, storage, cache invalidation, and page-level failures Thumbnails or tightly constrained fallback are required
Recreate as HTML A separately generated representation Layout and content can drift from the final PDF Two templates and two regression surfaces Accessible pre-invoice review is a distinct product surface
Custom PDF viewer Usually the same PDF bytes Evidence can remain intact if the original download is preserved More client code, compatibility testing, and security updates Search, annotations, or controlled navigation justify ownership

Embedding wins by default because it introduces the fewest representations. It does not solve every concern. Native browser viewers differ, mobile embedding can be awkward, and accessibility depends on the structure of the PDF as well as the viewer. Those are reasons to test the supported client matrix and provide a plain download action, not reasons to silently turn a signed invoice into screenshots.

One artifact. One digest.

Conversion is justified when the derived asset has a named purpose. Mark it as derivative, retain source_invoice_id, source_revision, source digest, converter build identifier, output digest, and creation time, then invalidate it whenever the source revision changes. Never display a revision-three thumbnail beside a revision-four download link. That failure is visually plausible and operationally dangerous because both requests return 200.

A custom viewer carries another boundary: PDF is an active, complex format, and the viewer parses untrusted bytes in a browser process. Follow the viewer project's security releases, apply a restrictive Content Security Policy to the containing application, isolate document origins where the threat model calls for it, and test malformed files. The PDF specification tells you how documents are represented; it does not remove the need to maintain the parser you ship.

Make the audit trail survive a dispute

An audit event is useful only if it distinguishes creation, finalization, signature, access authorization, delivery, and supersession. One vague invoice_viewed event cannot do that. Record server time, actor, action, invoice ID, revision, artifact digest, result, trace ID, and the policy decision that allowed or denied access. Protect the log according to its evidentiary value, with limited writers, retention rules, clock discipline, and a way to detect alteration.

Do not record more than the dispute requires. Invoice line items and addresses do not belong in an operational event when stable identifiers will join to controlled business records. This is a real trade-off: self-contained logs are convenient during an incident, while duplicated personal and financial data expands access and retention risk. Stable identifiers plus a protected lookup path are the more defensible default. Testing should cross layers. A unit test can confirm that a buyer cannot access another account's artifact. An integration test should stream a known fixture, exercise a range request, and verify headers plus digest. A browser test should cover the supported desktop and mobile matrix, including the download fallback. Finally, a restoration exercise should prove that artifact records and objects recover to matching revisions; a backup that restores the ledger without its PDFs, or PDFs without their ledger entries, has not restored the invoice system.

Deployment deserves the same suspicion. Roll out metadata changes before code that requires them, keep old artifact revisions readable for the retention period, and canary any converter or signer change against fixed fixtures. Compare bytes and signature validation results, not screenshots alone. Visual diffs catch layout regressions; hashes catch identity changes. They answer different questions.

The threshold can become the incident

Alert on a sustained failure ratio only when the denominator is large enough to be meaningful, and pair it with an absolute failure count so a quiet marketplace does not page on one transient request. There is no universal number to copy: derive the window and threshold from the service objective, traffic distribution, retry behavior, and the time an operator needs to prevent a breach. Test the rule against historical, non-customer-specific telemetry before enabling paging.

Keep integrity signals separate and sharper. One finalized ledger entry whose object is missing or whose digest differs can justify immediate investigation because averaging it into an availability percentage hides the exact event the audit design exists to prevent. In contrast, client cancellations, unsupported inline rendering, and authorization denials should have their own monitors and escalation paths.

The false-positive cost is concrete. A threshold that wakes someone whenever a native viewer abandons a byte-range request will train the on-call to distrust invoice alerts; suppressing all low-volume anomalies can hide a single altered artifact. Page for actionable loss of delivery or evidence integrity. Send browser-compatibility trends and capacity forecasts to daytime review. The quietest alerting system is not the goal. The goal is for the next page to identify a customer outcome, an artifact revision, and a first action before anyone opens a wall of charts.

Further reading

Top comments (0)