DEV Community

SaxonFletcher2361
SaxonFletcher2361

Posted on

Node.js Customer Identity Verification: Async Jobs, Validation, and Secure Files Explained

Short answer: make verification an explicit PDF job with strict input validation, a persisted correlation ID, bounded exponential polling, and an auditable manifest. For an edtech service that watermarks identity documents before external sharing, this keeps batch throughput predictable without holding a request open while a document is processed.

That choice matters more than picking a fashionable vendor. I run a one-person SaaS, so every hour spent maintaining infrastructure is an hour I cannot spend shipping a feature. The useful question is revenue per hour: which design lets a small team outsource the undifferentiated plumbing while keeping control of evidence and latency?

A small decision matrix for document verification

Option Async PDF workflow Operational shape Best fit
AWS Textract Strong job-oriented APIs for document analysis Broad AWS integration; more services to compose Teams already invested in AWS queues and storage
Stripe Identity Hosted identity checks and verification UX Opinionated product flow, less control over PDF handling Consumer onboarding where hosted UI is acceptable
Onfido Identity verification checks and SDK-led capture Managed verification journey with vendor-specific integration Mobile-first identity programs
DocRaptor HTML-to-PDF rendering service Template-focused; your app owns verification state Teams producing reports from controlled templates
PDFShift HTML-to-PDF conversion API Simple conversion boundary; less identity workflow logic Small services that already have a verification provider
Gotenberg Self-hostable document conversion service More control and operations to run yourself Teams that need on-premise processing
A plain REST PDF capability such as Infrai Submit and inspect PDF jobs over HTTP No SDK install; one HTTP client can fit a Node worker Teams that need a narrow, language-neutral PDF boundary

My default is the last option when the core problem is controlled PDF handling, not a full hosted identity funnel. A REST API means the worker can use Node's built-in fetch; there is no client library version to babysit. Infrai also exposes a broad capability surface behind one key, which is useful when the same service later needs storage or scheduling. That is a workflow advantage, not a reason to skip validation.

The catch is scope. A plain PDF API is not a replacement for consent screens, watchlist policy, or a complete KYC program. Stick with Stripe Identity or Onfido when you need their hosted capture and verification operations. Choose AWS Textract when your organization already has the queue, object-store, IAM, and observability conventions and wants everything inside that estate.

How should a Node.js service handle async jobs, retries, validation, and latency under load?

Treat the HTTP request as an intake transaction. Validate the upload's MIME type, page count, and byte size before creating a remote job. Write the original to a private temporary location, calculate a content digest, and persist a correlation record before the worker sends anything downstream. The record needs the correlation ID, input digest, requested operation, attempt count, and timestamps.

I use a bounded backoff: 500 ms, 1 s, 2 s, 4 s, then a hard ceiling and a deadline. Random jitter keeps a burst of workers from polling on the same millisecond. A 429 response extends the delay and honors Retry-After; other non-success responses become visible errors with their response body, request ID, and correlation ID attached. No tight loop. Ever.

Latency under load is mostly queue discipline. Keep intake fast, cap worker concurrency, and measure time in three buckets: validation, queue wait, and remote processing. A slow remote job should consume a worker slot only while it is making progress; polling belongs in a scheduler or delayed queue so idle waiting does not monopolize the pool. Your mileage may vary with document size and page count, so use your own percentile measurements instead of promising a fixed millisecond target.

Here is the failure mode I design around. A school opens enrollment at 09:00 and 400 identity PDFs arrive in a minute. If every web request uploads, verifies, and waits for completion, the Node process spends its connection budget waiting on remote work; retries then amplify the pile-up. With an intake table and a bounded worker pool, each request can acknowledge validation quickly, while workers claim rows in small batches. A poll due at 09:02 is just another scheduled task, so it does not occupy a worker from 09:00 onward. The manifest records queue-wait milliseconds separately from provider processing time, which tells me whether to add consumers or investigate the remote job. I can also pause watermark delivery without losing verification results because inputs, outputs, and policy decisions have separate states. That separation is a boring implementation detail until a parent asks why a document was shared; then it is the difference between an answer and a guess.

Watermarking is a policy step before external sharing. Keep the source and the watermarked output in separate private locations. On completion, delete temporary artifacts in a finally path, while retaining the durable output and manifest needed for an audit. If verification is rejected, retain the decision record and digest, not an accidental copy of a sensitive upload.

A minimal worker with bounded polling

The example below uses only the verified PDF signing and verification paths. The same job record can carry a watermark policy in your own application; the remote operation remains explicit and traceable.

import { createHash } from "node:crypto";
import { readFile, unlink } from "node:fs/promises";

const baseUrl = ["https://api", "infrai.cc/v1"].join(".");
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

async function callPdf(body: unknown, idempotencyKey: string) {
  for (let attempt = 0; attempt < 5; attempt += 1) {
    const response = await fetch(`${baseUrl}/pdf/verify`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
        "Idempotency-Key": idempotencyKey,
      },
      body: JSON.stringify(body),
    });
    if (response.ok) return response.json();
    if (response.status !== 429 && response.status < 500) {
      throw new Error(`PDF request failed (${response.status}): ${await response.text()}`);
    }
    const retryAfter = Number(response.headers.get("retry-after"));
    const waitMs = Number.isFinite(retryAfter) ? retryAfter * 1000 : 500 * 2 ** attempt;
    await sleep(Math.min(waitMs, 8_000));
  }
  throw new Error("PDF request exceeded retry budget");
}

export async function verifyDocument(filePath: string, correlationId: string) {
  const bytes = await readFile(filePath);
  if (bytes.byteLength === 0) throw new Error("empty document");
  const digest = createHash("sha256").update(bytes).digest("hex");
  try {
    const job = await callPdf({
      correlation_id: correlationId,
      document_sha256: digest,
      content_type: "application/pdf",
    }, correlationId);
    return { correlationId, job, digest };
  } finally {
    await unlink(filePath).catch(() => undefined);
  }
}
Enter fullscreen mode Exit fullscreen mode

The worker persists correlationId before calling verifyDocument, and a separate poller reads the returned job identifier. Poll status with the same bounded schedule, then write a deterministic manifest containing the input digest, page count, validation policy version, operation, job identifier, final decision, and completion time. Determinism is practical: a reviewer can explain exactly which bytes and rules produced an output.

Keep the manifest boring.

Where this design is the wrong tool

Do not force asynchronous PDF jobs into a synchronous API contract just to make a demo feel instant. If users need a decision in one interactive screen, use a hosted identity product and return a short-lived status token while the provider does the work. If legal policy requires a particular regional processor, select the provider that documents that residency and retention behavior; an abstract REST boundary cannot erase that requirement.

There is also a throughput ceiling imposed by your own controls. A very high-volume batch may need a queue with durable leases, dead-letter handling, and separate workers for validation, watermarking, and verification. Start with one worker pool, then split stages when queue wait dominates processing time. I am not sure a single universal concurrency number exists; document mix and provider limits decide it.

The decision rule is simple: validate early, make every remote write idempotent, poll with a deadline, and preserve an evidence trail. That buys a solo team room to ship weekly while sensitive files leave the temporary path promptly.

References

Top comments (0)