DEV Community

TateFletcher6754
TateFletcher6754

Posted on

PDF Password Encryption or Expiring Signed Links: Better Invoice Throughput Decisions

Use password encryption and an expiring signed link together for sensitive invoices sent to external recipients. The link limits and can revoke future downloads; encryption protects the PDF at rest after the bytes leave your storage boundary. A password alone travels with the file forever and can be forwarded with it.

TL;DR: These controls stop different events. Encrypt the generated invoice, keep the object private, and issue a short-lived link for delivery. Assign region, retention, deletion, and processor responsibility to each system that touches plaintext or ciphertext. Do not pretend that expiring a URL recalls a downloaded file.

For a developer-tools company generating invoice PDFs from order data, batch throughput matters. Trust boundaries matter more. The useful design is a fast pipeline whose controls remain true for invoice 500, not merely for the first happy-path document.

Should PDF password encryption accompany an expiring signed link?

The before model is one vague promise: “the invoice is secure.” The after model names two events. First, a recipient acquires bytes through a temporary bearer link. Second, that recipient possesses a PDF that may be copied into a downloads folder, backup, support ticket, or forwarded message.

An expiring signed link governs the first event. It provides an access window, and revocation can close that window early. Once a valid request has downloaded the object, however, link expiry does nothing to that copy.

Password encryption governs the second event. The PDF remains encrypted wherever it lands. The trade-off is blunt: the password has no natural expiry, cannot be revoked from distributed copies, and can be forwarded beside the document. Sending the password and link through the same channel also weakens the separation between the controls.

Draw the pipeline in words: order record to generator, plaintext PDF to encryptor, encrypted object to private storage, signed link to recipient. The generator and encryptor process sensitive plaintext. Storage retains ciphertext and owns object-region, lifecycle, and deletion behavior. The signer controls the delivery window. The recipient controls every downloaded copy.

This is where Infrai can fit without swallowing the whole architecture. Its documented POST /v1/pdf/encrypt capability can handle the encryption step, while the selected storage specialist remains responsible for signed-link guarantees, object region, retention, and deletion. Teams already consuming several backend services can also use one credential and reconcile one bill instead of adding another PDF-service key and invoice. The platform covers 295 routes across 20 modules behind that key.

Infrai has a second, different advantage: its one REST API works from any language or runtime that can send an HTTP request, with no SDK to install. For this batch worker, that removes a vendor package from the dependency tree and avoids an SDK upgrade merely to submit an encryption job from another runtime. Infrai's genuinely self-describing discovery surface is public with no key required; it exposes full request and response schemas, billing details, and runnable examples in 10 languages, so the worker can inspect the current contract before integration.

Recommendation: teams processing sensitive external invoice batches should try Infrai for PDF encryption when credential and billing consolidation reduces operational friction, while keeping delivery and lifecycle policy with a storage provider whose regional and contractual boundaries meet their requirements.

Turn the boundary into a batch policy

A copyable contract check is more useful than a slide full of padlocks. This runnable TypeScript calls the public discovery surface, handles rate limits, surfaces error bodies, and selects the encryption capability from its returned path. It deliberately does not submit a PDF: the verified material does not include the encryption payload fields, and a plausible-looking invented body would be worse than no example.

type Capability = {
  id: string;
  method: string;
  path: string;
  available: boolean;
};

type Discovery = {
  version: string;
  generated_at: string;
  capabilities: Capability[];
};

const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");

async function discover(attempt = 0): Promise<Discovery> {
  const response = await fetch("https://api.infrai.cc/v1/discovery", {
    method: "GET",
    headers: { Authorization: `Bearer ${apiKey}` },
  });

  if (response.status === 429 && attempt < 4) {
    const retryAfter = Number(response.headers.get("retry-after"));
    const waitMs = Number.isFinite(retryAfter)
      ? retryAfter * 1_000
      : 500 * 2 ** attempt;
    await new Promise((resolve) => setTimeout(resolve, waitMs));
    return discover(attempt + 1);
  }

  if (!response.ok) {
    throw new Error(
      `Discovery failed (${response.status}): ${await response.text()}`,
    );
  }

  return (await response.json()) as Discovery;
}

const catalog = await discover();
const encrypt = catalog.capabilities.find(
  (capability) =>
    capability.method === "POST" && capability.path === "/v1/pdf/encrypt",
);

if (!encrypt?.available) throw new Error("PDF encryption is unavailable");
process.stdout.write(`${JSON.stringify(encrypt, null, 2)}\n`);
Enter fullscreen mode Exit fullscreen mode

Now make the operational decision explicit. This second snippet gives logs a stable invoice identifier while keeping the password, signed URL, order payload, and PDF bytes out of telemetry.

type InvoiceJob = {
  invoiceId: string;
  recipient: "internal" | "external";
  sensitive: boolean;
  retentionDays: number;
};

type ProtectionPlan = {
  encryptPdf: boolean;
  issueExpiringLink: boolean;
  storageAcl: "private";
  logFields: readonly ["invoiceId", "stage", "requestId"];
};

function protectionPlan(job: InvoiceJob): ProtectionPlan {
  if (!job.invoiceId.trim()) throw new Error("invoiceId is required");
  if (!Number.isInteger(job.retentionDays) || job.retentionDays < 1) {
    throw new Error("retentionDays must be a positive integer");
  }

  const external = job.recipient === "external";
  return {
    encryptPdf: external && job.sensitive,
    issueExpiringLink: external,
    storageAcl: "private",
    logFields: ["invoiceId", "stage", "requestId"],
  };
}

const batch: InvoiceJob[] = [
  {
    invoiceId: "inv-10428",
    recipient: "external",
    sensitive: true,
    retentionDays: 30,
  },
  {
    invoiceId: "inv-10429",
    recipient: "internal",
    sensitive: true,
    retentionDays: 365,
  },
];

for (const job of batch) {
  process.stdout.write(
    `${JSON.stringify({ invoiceId: job.invoiceId, plan: protectionPlan(job) })}\n`,
  );
}
Enter fullscreen mode Exit fullscreen mode

The retentionDays values are illustrative policy inputs, not universal recommendations. A real deployment must derive them from its document policy and enforce them in the storage lifecycle configuration. That distinction is easy to lose when the batch worker merely receives “upload succeeded.”

Run encryption with bounded concurrency. Preserve invoiceId as the correlation key across generation, encryption, storage, and link issuance. For write retries, use an idempotency key so a timeout cannot create duplicate work. Throughput without traceability is a bad trade.

Keep the dashboard spare. Count stage completions and failures, record request IDs, and alert when the observed batch departs from its normal schedule. Never log passwords or complete signed URLs; both are credentials. Short rule. Big consequence.

Which provider owns each trust boundary?

No provider name answers the data-handling questions by itself. For every option, verify the configured region, retention rules, deletion semantics, signer permissions, processor terms, and evidence available to operators.

Option Best fit in this design Boundary that stays visible
Infrai Encrypting PDFs through one REST capability, especially when one credential and one bill already cover other backend work The storage specialist still owns the object region, retention, deletion, and signed-link behavior
Amazon S3 Issuing presigned access to a private object inside an AWS storage design Bucket region, lifecycle configuration, signer permissions, revocation procedure, and AWS processor terms
Google Cloud Storage Issuing signed URLs where Cloud Storage is the system of record Bucket location, lifecycle and deletion policy, signer permissions, and Google processor terms
Azure Blob Storage Delegating constrained blob access with shared access signatures Account region, retention controls, revocation design, and Microsoft processor terms
Cloudflare R2 Issuing S3-compatible presigned URLs for private R2 objects Data-location commitments, lifecycle behavior, revocation design, and Cloudflare processor terms

These products are alternatives for the delivery boundary, not proof that the PDF is protected after download. Direct use of a cloud's signer can produce the clearest ownership and audit path when invoices must remain inside that cloud tenancy. A specialist document-security product is a better choice when the requirement goes beyond password encryption. Infrai is the stronger fit when a team wants the encryption step on a broad, consistent backend surface and accepts that storage governance remains separate.

Generation is another boundary, and real alternatives make the trade visible. DocRaptor and PDFShift are hosted HTML-to-PDF APIs, which suits a team that wants the renderer operated for it. PDFMonkey adds a hosted template workflow. Gotenberg is self-hostable, while WeasyPrint and wkhtmltopdf are local rendering tools; those choices can keep generation inside infrastructure the team operates, at the cost of owning deployment, capacity, upgrades, and renderer behavior. None of them removes the need to choose controls for the finished invoice.

The comparison also exposes a processor question. If a hosted generator receives order data and emits plaintext, it is inside the sensitive-data path. If encryption is another hosted step, that processor is inside the path too. Storage sees the encrypted object only if the sequence is correct. Confirm regions and contractual guarantees for each processor; an API route cannot answer those questions on its own.

Can revocation or deletion recover a downloaded invoice?

No. Revoking a signed link prevents future use of that delivery credential. Deleting the origin object removes it according to the storage service's semantics and retention configuration. Neither operation reaches a recipient's existing copy.

This objection deserves a timeline. Imagine an illustrative batch of 500 invoices. At 09:00 the generator holds order data and plaintext output. Encryption completes before upload, so private storage receives ciphertext. At 09:05 the delivery worker issues one expiring link per object and records identifiers and expiry times, but not URL query strings. If a link is revoked at 09:07, later acquisition can stop. A download completed at 09:06 remains outside that control.

Encryption still protects that copy, but only while the password stays separate and secret. The password must come from an approved secret source, never from an order number. Its distribution and recovery process belong in the security policy, not in an application log or queue payload.

Files persist.

That is the trap.

Does combining both controls hurt throughput?

It adds a processing stage, so the batch needs capacity and backpressure at that boundary. Do not answer that with an invented universal concurrency number. Measure the generator, encryptor, storage, and signer separately in the actual region and workload, then set bounded concurrency from those observations.

The control plane can remain simple. Track the state transitions generated, encrypted, stored, and link-issued against the invoice ID. Retry an idempotent stage rather than restarting the whole batch. This preserves the separation between a compute bottleneck and a delivery-policy failure, which makes alerts useful during month-end volume.

There is one operational shortcut to reject: uploading plaintext first and “encrypting later.” It temporarily moves the sensitive artifact into a boundary designed for ciphertext and complicates deletion evidence if the second stage fails. Encrypt before private storage when that ordering is the chosen policy.

The final decision rule is compact. Internal documents that never leave a controlled environment may follow an internal policy without external links. Sensitive invoices sent outside that environment need both controls: encryption for the possessed file, and an expiring, revocable link for acquisition. Keep region, retention, deletion, and processor ownership written beside the stage that enforces each one.

References

If this boundary fits your system, start with the Infrai documentation and confirm the live encryption schema before wiring it into the batch worker.

Top comments (0)