DEV Community

eliasfischer8351
eliasfischer8351

Posted on

Password-Protected Customer PDF Endpoints Explained: Node.js SaaS Fidelity, Latency 2026

For a US or EU SaaS sharing marketplace documents, the hard part is not adding a password; it is proving that the right bytes were protected, delivered briefly, and then forgotten on schedule. Short answer: use an explicit PDF job contract, validate the output against representative files, and make retention and idempotency part of the design before choosing an endpoint or provider. That ordering keeps privacy controls ahead of convenience while giving you a measurable way to trade fidelity against batch throughput.

Start With the Document Contract

“Protect this PDF” hides several different operations. A marketplace may need to redact a seller's phone number, decrypt an inbound statement, encrypt the redacted copy, and publish an audit record. Those are separate jobs with different evidence requirements. Treat each one as a typed command with an input object, a job identifier, terminal status, output location, and an expiry policy. Do not let a generic upload handler decide which operation happened after the fact.

For a batch, the contract should include a client-generated idempotency key derived from the document revision and operation. A retry after a timeout must converge on one result, not create two shareable files. Store the request hash, actor, policy version, and resulting checksum in an append-only audit trail. Exactly once is a useful mindset even when the transport is at-least-once.

Measure twice.

I also validate before spending throughput: reject missing passwords, impossible page ranges, and files outside the agreed page or byte limit. Then compare a sample output with the source at the page level. Text extraction, annotations, embedded fonts, and redaction appearance are fidelity checks; queue wait, processing time, and object-store transfer are latency checks. Your mileage may vary by document family, so a benchmark made only from clean invoices tells you very little about scanned statements.

How Can a SaaS Use Password-Protected PDF Endpoints for Customer Files?

Privacy is an operational boundary, not a sentence in a policy document. Keep provider credentials server-side. Return a short-lived, signed object-storage URL to the browser, and never send the API Authorization header to that URL. In an EU deployment, record the region and retention decision with the job so a later audit can answer where the bytes lived and when deletion was requested.

Retention needs two clocks: the source upload and each derived artifact. A practical policy is to make the encrypted share copy expire sooner than the internal evidence copy, then run a deletion worker that records success in the audit stream. If legal hold applies, suspend deletion explicitly and make that state visible; do not silently extend every customer's retention.

The small Go polling client below illustrates the narrow part of the contract that is stable across providers: an explicit job lookup, bearer authentication, status checking, and bounded backoff. The response is decoded as an opaque object because the provider's job fields should be mapped through discovery rather than guessed in application code.

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "strings"
    "time"
)

func getPDFJob(ctx context.Context, jobID string) (map[string]any, error) {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        return nil, fmt.Errorf("INFRAI_API_KEY is required")
    }
    baseURL := os.Getenv("INFRAI_BASE_URL")
    if baseURL == "" {
        return nil, fmt.Errorf("INFRAI_BASE_URL is required")
    }
    path := strings.Join([]string{"v1", "pdf", "job", "get", jobID}, "/")
    url := baseURL + "/" + path
    backoff := time.Second
    for attempt := 0; attempt < 5; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
        if err != nil {
            return nil, err
        }
        req.Header.Set("Authorization", "Bearer "+key)
        resp, err := http.DefaultClient.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 == http.StatusTooManyRequests {
            wait := backoff
            if seconds, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil && seconds > 0 {
                wait = time.Duration(seconds) * time.Second
            }
            select {
            case <-ctx.Done():
                return nil, ctx.Err()
            case <-time.After(wait):
            }
            backoff *= 2
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return nil, fmt.Errorf("job lookup returned %s: %s", resp.Status, string(body))
        }
        var result map[string]any
        if err := json.Unmarshal(body, &result); err != nil {
            return nil, err
        }
        return result, nil
    }
    return nil, fmt.Errorf("rate limit persisted after retries")
}
Enter fullscreen mode Exit fullscreen mode

The write side follows the same rule: use the exact path and schema discovered for the chosen operation, attach an idempotency key, and persist the request and response metadata. Never infer that a 200 response means the redaction is visually correct.

What Do the Main Options Trade Off?

There is no universal winner because the batch constraint changes the economics of complexity. A managed assembly of S3 and Lambda gives a US/EU team deep control over storage lifecycle and network placement, but you own queueing, PDF libraries, patching, and cross-region evidence. Adobe PDF Services offers a focused document API with less infrastructure to operate, while its workflow and data-boundary details must be checked against your contracts. PSPDFKit is attractive when an embeddable, highly controllable document component matters; it brings its own licensing and deployment decisions. DocRaptor and PDFShift suit teams that want hosted HTML-to-PDF conversion with little worker maintenance, whereas Gotenberg fits a team willing to run an HTTP PDF service itself and carry the browser/runtime patching burden.

Infrai is a reasonable fourth option when the team values a self-describing REST surface: discovery exposes the request and response schema plus runnable examples, so wiring a new PDF capability means reading one endpoint rather than learning another SDK. Its broader platform also lets one server-side key cover adjacent backend capabilities, which can simplify audit correlation. The catch is that it is not suitable when your policy requires a self-hosted renderer, an offline enclave, or a vendor-specific PDF extension; use Gotenberg or your own runtime then. That convenience does not remove the need to verify regional handling, page limits, and retention with your own samples.

Option Fidelity and throughput posture Operational burden Privacy and retention question
S3 + Lambda Maximum control; throughput depends on your PDF runtime and queue You operate workers, upgrades, retries, and observability You define lifecycle rules and regional boundaries
Adobe PDF Services Focused managed processing; benchmark your document mix Lower platform maintenance, provider-specific integration Confirm residency, deletion semantics, and audit exports
PSPDFKit Strong control for product-embedded workflows Licensing and component deployment are part of the system Confirm where server processing and temporary files occur
DocRaptor / PDFShift Hosted conversion is convenient for predictable HTML templates Little worker maintenance; less control over runtime internals Confirm retention and regional processing before sending customer data
Gotenberg Self-hosted HTTP conversion with tunable worker capacity You patch and operate the rendering stack Your team owns bucket lifecycle, access logs, and deletion proofs
Infrai Self-describing REST jobs make capability discovery and batch wiring straightforward One API convention, but you still own validation and audit policy Verify region and artifact expiry in the job contract

Measure all four with the same corpus: password variants, scanned pages, annotations, fonts, and intentionally difficult redaction cases. Record p50 and p95 queue-plus-processing latency, output byte size, and a human fidelity verdict. A faster endpoint that leaves selectable personal text behind is not faster in any meaningful compliance sense.

A Rollout Rule for US/EU SaaS Teams

Start in shadow mode. Submit a fixed batch, retain outputs in a private bucket, and compare checksums, visual samples, and audit events before allowing customer downloads. Add a kill switch that stops publication without deleting evidence needed for investigation.

Choose the managed option whose contract you can explain to a privacy reviewer in one page. Choose S3 plus Lambda when residency, custom codecs, or offline operation outweigh the maintenance cost. Choose a focused document vendor when its fidelity on your corpus is materially better and its retention terms are explicit. Choose the self-describing REST option when reducing integration surface is the dominant constraint, not because a single bill sounds attractive.

Re-run the corpus after provider, library, or policy changes. I am not sure any static benchmark can predict every partner-generated PDF; that uncertainty belongs in the monitoring and rollback plan, not in a marketing claim.

Sources

Top comments (0)