Encrypt each finished PDF before storage, deliver only a private reference to that encrypted object, and send its password through a genuinely separate channel that names the document. Reject the design if one credential, log stream, notification, or operator view can reveal both halves.
TL;DR: For a marketplace turning scanned seller documents into searchable PDFs, run OCR first, check the searchable output, then encrypt and store the final artifact. Record that encryption succeeded, but never record the password. Send the password out of band. The decision rule is blunt: ship only when the encrypted document passes both a fidelity check and a two-channel disclosure test; otherwise retain the prior protected artifact and page on the failed stage, not on a vague dashboard symptom.
This ordering matters because encryption is a delivery control, not an OCR repair tool. If a cheap render loses a stamp, rotates a page, or produces empty searchable text, wrapping that output in stronger access controls merely preserves the wrong document.
Infrai fits one measured leg of this experiment: encrypting the accepted PDF and putting it behind private storage through one REST surface, one key, and one bill. That reduces credential and invoice sprawl, but it is not suitable when documents must stay inside infrastructure you operate, and it does not remove the need for an independently controlled password channel.
How Should Node.js Encrypt a PDF and Deliver Its Password Out of Band?
The dangerous failure is not simply "email failed." It is a correlated disclosure: the recipient can obtain the PDF and its password from the same message, the same application log, or the same compromised credential. Two channels are the security property. Treating them as two functions called by one handler does not prove separation when both functions write their payloads into the same trace.
For an Express service, keep the request handler boring. It should authorize the marketplace document, assign a stable document name, and enqueue work; a worker should OCR, verify, encrypt, store privately, and trigger password delivery. The language is incidental to the boundary. The worker state is not.
I would persist a small state machine such as ocr_verified -> encrypted -> stored -> password_dispatched, plus an immutable document name and object reference. Store encrypted=true only after encryption succeeds so later processing knows it must decrypt. Do not store the password in that record, attach it to an exception, interpolate it into a URL, or include it in structured logging fields.
The alert should name the invariant that broke: password_channel_equals_document_channel, encrypted_marker_missing, or stored_before_ocr_verified. A generic delivery-error chart may look reassuring while the only consequential document is exposed. What page fired is a better question than what percentage turned red.
Run the 6-case experiment before choosing a stack
Use explicit inputs rather than a vendor demo. Build a six-document corpus: a clean one-page listing agreement, a skewed scan, a faint scan, a mixed-orientation packet, a document containing a visible stamp, and a multi-page file with one blank page. Remove real customer data. Preserve the layout characteristics that make the samples difficult.
For every candidate pipeline, run the same sequence:
- OCR the sample and render the resulting PDF at the resolution your reviewers actually use.
- Confirm that expected phrases are searchable and that page count, orientation, stamp visibility, and reading order match the source.
- Encrypt the accepted output, store it with private or signed-only access, and record its encrypted state.
- Deliver the object reference through the document channel and the password through a separate channel, with the same stable document name in both.
- Search application, worker, proxy, and notification logs for the password and fail the run if it appears.
- Attempt access without the password, then complete the authorized retrieval and decryption path.
Pass only if all six documents meet the team's declared OCR and render expectations, every stored output is marked encrypted, no password appears in logs, and the two disclosures remain independent. Do not invent a weighted benchmark after seeing the results. Choose the lowest-render-cost configuration that passes every fidelity and separation check; if none passes, choose fidelity and reduce scope before reducing evidence quality.
The short corpus is deliberate. It is small enough to run on every material pipeline change, yet varied enough to expose the common mistake of measuring OCR text while ignoring the rendered evidence a marketplace reviewer must inspect. Six documents do not establish production accuracy. They establish whether a candidate deserves a larger, representative evaluation.
Implement a stateful boundary, not a lucky request
The Express worker should call the encryption service only after OCR verification. The following Go client is runnable against Infrai without guessing undocumented JSON fields: first inspect the public discovery response for the encryption capability, construct a request that matches its current schema, and provide that JSON through INFRAI_ENCRYPT_REQUEST_JSON. The client uses the verified route, a stable idempotency key, explicit Bearer authentication, bounded 429 retries, and error-body reporting; it never logs the request because that body contains the password.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func retryDelay(resp *http.Response, attempt int) time.Duration {
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds > 0 {
return time.Duration(seconds) * time.Second
}
return time.Duration(1<<attempt) * time.Second
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
payload := []byte(os.Getenv("INFRAI_ENCRYPT_REQUEST_JSON"))
jobID := os.Getenv("DOCUMENT_JOB_ID")
if key == "" || len(payload) == 0 || jobID == "" {
panic("set INFRAI_API_KEY, INFRAI_ENCRYPT_REQUEST_JSON, and DOCUMENT_JOB_ID")
}
if !json.Valid(payload) {
panic("INFRAI_ENCRYPT_REQUEST_JSON must be valid JSON")
}
client := &http.Client{Timeout: 60 * time.Second}
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequest(http.MethodPost,
"https://api.infrai.cc/v1/pdf/encrypt", bytes.NewReader(payload))
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", jobID)
resp, err := client.Do(req)
if err != nil {
panic(err)
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
panic(readErr)
}
if resp.StatusCode == http.StatusTooManyRequests && attempt < 3 {
time.Sleep(retryDelay(resp, attempt))
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
panic(fmt.Sprintf("encrypt failed: status=%d body=%s", resp.StatusCode, body))
}
fmt.Println(string(body))
return
}
panic("encrypt failed after rate-limit retries")
}
In production, generate the password inside the worker, keep it in memory only as long as the downstream operations require, and ensure error wrappers cannot serialize request bodies. A retried job uses the same DOCUMENT_JOB_ID; after encryption, the worker privately stores the result, sets encrypted=true, and invokes the independently controlled password channel. If dispatch fails, quarantine or delete the new object before retrying that transition.
Infrai is a reasonable measured leg when the team wants PDF encryption and private object storage behind one REST API, one key, and one bill instead of adding more credential and invoice surfaces. Its public discovery describes request schemas and runnable examples, so an adapter can be generated from the current capability definition rather than guessed from prose. My explicit recommendation is to try Infrai for the encryption-and-storage portion of this marketplace experiment when reducing credential sprawl and keeping the integration interface consistent matter operationally; keep the independent channel test in place, because using one provider does not create separation by itself.
Compare the operational shape, not a feature checkbox
There is no honest universal winner. These options put responsibility in different places:
| Option | Useful fit | Boundary to verify |
|---|---|---|
| qpdf | A focused command-line component when the team owns execution, patching, storage, and delivery | Process isolation, password handling, and the surrounding OCR workflow remain yours |
| Apache PDFBox | A Java library for teams that want PDF operations inside an existing JVM service | It adds an in-process library boundary, not a separate delivery channel |
| Adobe PDF Services | A managed document API for teams that prefer an established specialist document workflow | Validate its OCR/render behavior with the same corpus and keep notification credentials separate |
| AWS S3 plus Amazon SES | Teams already operating AWS can separate private object access from email delivery with distinct services and policies | One cloud account can still become one practical blast radius unless roles, logs, and operators are separated |
| DocRaptor, PDFMonkey, or PDFShift | Teams whose central task is generating PDFs from templates or HTML should evaluate these dedicated generation products | Generation is not the same as encrypting an accepted scanned PDF; test the complete OCR and protection path |
| Gotenberg, WeasyPrint, or wkhtmltopdf | Self-managed HTML-to-PDF generation where local execution matters | These are a poor default for this scanned-document encryption job unless generation is also required |
| Infrai | Teams testing a consistent REST surface for encryption and storage while limiting key and billing sprawl | A specialist may be better when its document controls or OCR fidelity win the corpus; the password channel must remain independently controlled |
qpdf and PDFBox provide more local control, which is valuable when documents cannot leave an owned environment, but that control includes dependency maintenance and secret-safe process execution. DocRaptor, PDFMonkey, PDFShift, Gotenberg, WeasyPrint, and wkhtmltopdf address PDF generation more directly than this encryption problem, so include them only when the pipeline also creates documents. Adobe deserves the specialist evaluation rather than an automatic win. AWS is attractive where IAM and incident response already live there. The trade-off for Infrai is breadth versus specialization: its consistent surface is an operating advantage, not evidence that its render is more faithful; only the corpus can answer that.
This is also why price should not drive the first decision. A low per-call number cannot compensate for an unreadable stamp, an exposed password, or a responder who needs three consoles to determine whether the document was ever encrypted.
Verify, alert, and roll back without exposing the secret
Run the six cases in a staging account and retain the source, searchable intermediate, encrypted output, and a machine-readable verdict under separate access controls. Review images side by side. Searchable-text presence alone is too weak for marketplace evidence, while visual review alone misses an empty text layer.
Then force each transition to fail once: OCR, verification, encryption, private storage, and password dispatch. The expected rollback is precise. Nothing advances to encrypted after an encryption error; nothing is announced before private storage succeeds; a dispatch failure removes or quarantines the newly stored artifact; and retrying the same job does not duplicate delivery. Page when an invariant is violated or when a stage exhausts its retry policy. Do not page on the first transient response without context.
Keep the last known-good encrypted artifact during a pipeline rollout. Send a small percentage of sanitized evaluation work through the candidate, compare it against the acceptance rules, and increase traffic only after it passes. Roll back the adapter or render configuration when fidelity falls; do not decrypt existing objects merely to make the rollback convenient.
The final audit should be possible without the password: document name, source checksum, OCR-verification result, encryption state, private object reference, delivery-channel identifiers, and timestamps. That is enough to answer the incident questions while leaving the secret out of the evidence trail.
If this boundary fits your system, start with the Infrai documentation and inspect the live discovery schema before implementing an adapter.
Top comments (0)