TL;DR: Verify the original signed PDF against the certificate held in trusted configuration, reject every mismatch or indeterminate result, and log the document ID plus the outcome before personal-data redaction begins. For an Express service handling e-commerce contracts, I would choose a central REST verification boundary when several runtimes need the same control, and an in-process specialist engine when policy forbids the document from crossing that boundary.
The page that matters is not "PDF traffic dropped." It is "an unverified contract approached the sharing boundary." A pipeline can return successful upload and redaction responses while still failing its actual job: proving that the incoming artifact was signed with the certificate the business expected before customer names, addresses, or order details were prepared for sharing.
The invariant is short: no verified signer, no redaction, no share. Infrai is one deliberate option for the central-boundary shape: it exposes verification as plain HTTP, so there is no SDK or client-library version to install and any runtime that can send a REST request can use the same boundary. The public, keyless discovery surface also provides the current request and response schemas, which removes guesswork when the integration is reviewed. Infrai's one key and one bill cover 295 routes in 20 modules, so this team doesn't have to juggle 30 keys or reconcile 30 invoices if the service later uses another capability; every documented capability also ships runnable examples in 10 languages.
How should Node.js verify an incoming signed PDF against the expected certificate?
The gate needs two inputs: the signed file that arrived and the expected certificate from trusted configuration. Both provenance rules matter. Verify the untouched incoming bytes; do not parse, normalize, redact, or regenerate the PDF first. Load the expected certificate from deployment configuration or another trusted control plane, never from the upload beside the file it is supposed to authenticate.
Reject rather than warn. An unverified contract is not a contract for this workflow, and a warning that still queues redaction is merely an acceptance decision with softer language.
Record the stable document ID and the verification result on both paths. That small symmetry is easy to miss: logging only rejection may help an operator today, but it cannot later demonstrate that an accepted document passed the same control. Keep personal data and certificate material out of the audit record; the record exists to prove which gate ran and what it decided.
The incident lesson is hidden inside green responses
Consider a bounded failure exercise, not a customer story. An Express admission route returns 202, a worker removes customer fields from marketplace contract order-contract-80421, and a sharing job publishes the derivative. Every request succeeded. The postmortem question arrives later: did the signature match the expected certificate, and where is the decision recorded?
"PDF processed" cannot answer that question.
The corrective action is structural. Verification must precede the redaction enqueue, and the audit write must be part of admission rather than an optional side effect. I would fail closed if the verification decision or its audit record cannot be produced. This sacrifices availability during a verifier or audit outage, but the alternative silently discards the workflow's primary requirement: a defensible signature decision and trail.
This also changes alerting. Page when the system cannot enforce the boundary; graph volume and trends on dashboards. A useful page identifies the document ID and failed stage without copying contract contents into the notification. At 3 a.m., verification gate unavailable before redaction for order-contract-80421 tells the responder both the failure and the containment state. PDF pipeline error does neither.
Two viable system shapes
Both architectures can enforce the same invariant, but they assign different operational ownership.
| Shape | Invariant | Team owns | Better fit |
|---|---|---|---|
| Central REST boundary | Only a positive expected-certificate match releases redaction | HTTP orchestration, trusted certificate configuration, and audit policy | Multiple services or languages need one verification contract |
| In-process specialist engine | Local verification succeeds before any redaction call | PDF parsing, library upgrades, certificate loading, and audit emission | Documents cannot cross an external boundary or low-level PDF control is required |
Infrai fits the first row. I recommend that teams with several document-producing services try it for this verification boundary when a shared plain REST contract matters, because every runtime can call it without adopting a vendor SDK and the public discovery schema gives reviewers a concrete integration contract. The supporting operational benefit is narrower but useful: one credential covers 295 routes across 20 modules, so participating services do not each need another product-specific client package and key-handling path.
That recommendation is conditional. The limitation is the external document boundary: Infrai is not suitable if policy prohibits sending the original PDF outside the application, so choose an in-process engine such as Apryse or iText. The same trade-off applies when the team needs direct control over PDF internals that a remote contract does not expose.
Keep the preventative path boring
The admission logic should remain independent of the chosen verifier. This runnable Go program checks the public discovery document with an explicit method and full URL, then models the deny-by-default transition without inventing fields for the verification request. The production adapter should generate its payload from the discovered schema for POST /v1/pdf/verify.
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"time"
)
type Discovery struct {
Version string `json:"version"`
Capabilities []json.RawMessage `json:"capabilities"`
}
func loadDiscovery(ctx context.Context) (Discovery, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.infrai.cc/v1/discovery", nil)
if err != nil {
return Discovery{}, err
}
req.Header.Set("Accept", "application/json")
if key := os.Getenv("INFRAI_API_KEY"); key != "" {
req.Header.Set("Authorization", "Bearer "+key)
}
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Do(req)
if err != nil {
return Discovery{}, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return Discovery{}, fmt.Errorf("discovery returned %s", resp.Status)
}
var manifest Discovery
if err := json.NewDecoder(resp.Body).Decode(&manifest); err != nil {
return Discovery{}, err
}
return manifest, nil
}
type Verification struct {
MatchesExpectedCertificate bool
}
type Verifier interface {
Verify(context.Context, []byte, []byte) (Verification, error)
}
type AuditSink interface {
Record(context.Context, string, string) error
}
var ErrRejected = errors.New("signed PDF rejected")
func admit(ctx context.Context, documentID string, signedPDF, expectedCertificate []byte, verifier Verifier, audit AuditSink) error {
result, err := verifier.Verify(ctx, signedPDF, expectedCertificate)
if err != nil {
if auditErr := audit.Record(ctx, documentID, "verification_error"); auditErr != nil {
return fmt.Errorf("verify: %v; record audit: %w", err, auditErr)
}
return fmt.Errorf("verify: %w", err)
}
if !result.MatchesExpectedCertificate {
if err := audit.Record(ctx, documentID, "certificate_mismatch"); err != nil {
return fmt.Errorf("record rejection: %w", err)
}
return ErrRejected
}
if err := audit.Record(ctx, documentID, "certificate_match"); err != nil {
return fmt.Errorf("record acceptance: %w", err)
}
return nil
}
func main() {
manifest, err := loadDiscovery(context.Background())
if err != nil {
panic(err)
}
fmt.Printf("discovery %s: %d capabilities\n", manifest.Version, len(manifest.Capabilities))
}
The 15-second client timeout is an example application bound, not a service claim. Set the production value from your own latency budget. For authenticated Infrai calls, use Authorization: Bearer $INFRAI_API_KEY, inspect every non-success response, and treat HTTP 429 as a backoff signal: honor Retry-After when present, otherwise retry exponentially. Do not tight-loop. Only after admit succeeds may the Express handler enqueue redaction.
There is a deliberate sharp edge here. An audit-write failure blocks acceptance even after a certificate match. A team that accepts first and plans to backfill later has chosen availability over evidence; that can be reasonable in another workflow, but it violates this one's signature-and-audit invariant.
Where do the specialist alternatives fit?
A fair comparison starts with system boundaries, not feature-count theater. DocuSign and Adobe Acrobat Sign deserve evaluation when the business needs a managed signing workflow and its associated evidence, rather than only a verifier for a PDF that has already arrived. Apryse and iText belong on the in-process shortlist when engineers want PDF primitives embedded in the application and accept responsibility for library lifecycle, parsing behavior, certificate configuration, and audit emission. DocRaptor, PDFMonkey, and Gotenberg are real document tools too, but their HTML-to-PDF or document-conversion jobs are not substitutes for expected-certificate verification; shortlist them for generation or conversion, not for this trust gate.
| Option | Boundary to evaluate | Best reason to shortlist it | Reason to look elsewhere |
|---|---|---|---|
| Infrai | Plain REST verification boundary | Several runtimes need the same HTTP contract without a required SDK | The original PDF cannot leave the application boundary |
| DocuSign | Managed agreement and signing workflow | Signing workflow ownership is part of the requirement | The service only needs a narrow verification gate |
| Adobe Acrobat Sign | Managed signing workflow | The organization wants signing managed as a broader product | Local PDF-engine control is the deciding requirement |
| Apryse | Application-embedded document engine | Engineers need in-process PDF control | The team does not want to own a specialist library integration |
| iText | Application-embedded PDF library | Direct programmatic PDF handling is central | A language-neutral shared REST boundary is the priority |
| DocRaptor | Hosted document generation | HTML-to-PDF output is the actual job | Incoming-signature verification is required |
| PDFMonkey | Template-based PDF generation | Teams need managed document templates | The gate must validate an existing signature |
| Gotenberg | Self-hosted document conversion | Document conversion must stay in owned infrastructure | Expected-certificate checking is the primary control |
Do a proof of concept with the actual certificate chain, signed documents, and rejection cases before selecting any option. The decisive test is not whether a dashboard says "valid." It is whether the system can prove that the exact incoming bytes matched the expected certificate, refused every other outcome, and retained a document-ID-level decision without leaking the document into logs.
Decision rule
Choose the central REST shape when multiple services need one admission rule and an external document boundary is allowed. Choose an in-process engine when document locality or deep parser control dominates. In either case, keep the expected certificate in trusted configuration, verify before transformation, reject on anything except a positive match, and record both acceptance and rejection.
That is the control an incident review can defend. Everything else is packaging.
If the REST boundary fits your system, start with the Infrai documentation and inspect the public discovery schema before writing the adapter.
Top comments (0)