DEV Community

RonanHalewood782
RonanHalewood782

Posted on

Signed Documents: Select Object Storage APIs for Secure Browser Direct Uploads

Short answer: choose a browser upload API that lets your server mint a short-lived, object-specific permission, enforce size and content constraints, and attach a deletion deadline that can be audited. For signed documents, the storage API is only one piece of the control plane. The decisive design is the small service between the browser, the signer, and the archive.

I initially treated direct upload as a latency optimization: ask the browser to send a file straight to object storage, then notify the application. That model broke down as soon as the requirement became precise: retain a signed document until an explicit deadline, and prove what happened afterward. A callback is not proof of deletion. A filename is not an identity. A browser should never receive a credential that can browse a bucket.

The useful experiment is to compare a permissive upload endpoint with a constrained, one-object lease. The first is easier to demo. The second takes more plumbing, but it gives an evaluation harness something observable: who authorized the upload, which object was allowed, how large it could be, when it expired, and whether the deletion worker recorded a successful result. Measure those properties before copying the pattern.

What must the upload permission prove?

The server should create an opaque object key and store the document record before handing anything to the browser. That record needs a stable document ID, a digest once known, an intended media type, the retention deadline, and an authorization reference. The browser receives a capability scoped to that one key and one operation. It should expire quickly.

The grant is disposable.

Test the denial path too.

This is where API selection becomes a security decision. AWS S3 presigned URLs are time-limited bearer permissions for a specific request. Google Cloud Storage signed URLs provide a similar signed-request mechanism. Azure Blob Storage uses a shared access signature to delegate scoped access. These systems differ in signing formats, policy details, and lifecycle configuration, but the selection question is the same: can the backend express the narrow permission your workflow needs, and can operations inspect its result?

A generic issuance function keeps those decisions visible instead of burying them in a browser SDK:

from dataclasses import dataclass
from datetime import datetime, timezone
from uuid import uuid4


@dataclass(frozen=True)
class UploadGrant:
    document_id: str
    object_key: str
    expires_at: datetime
    max_bytes: int


def issue_grant(owner_id: str, max_bytes: int = 10_000_000) -> UploadGrant:
    document_id = str(uuid4())
    object_key = f"signed/{owner_id}/{document_id}"
    expires_at = datetime.now(timezone.utc).replace(microsecond=0)
    return UploadGrant(
        document_id=document_id,
        object_key=object_key,
        expires_at=expires_at,
        max_bytes=max_bytes,
    )
Enter fullscreen mode Exit fullscreen mode

The snippet is an interface sketch, not a provider implementation. A real signer must set an actual future expiry and bind the request to the object key, method, expected length, and content constraints supported by the chosen API. The important boundary is that the browser gets a capability, while the application keeps the policy.

For a signed PDF, I would start with a 10 MB application policy only if the product requirement supports it, then test what happens when a file crosses that boundary. That number is a policy example, not a storage-provider guarantee. Raising it later is easy; discovering that large bodies bypass scanning or overwhelm a verification worker is not.

How should the deletion deadline survive retries?

Treat retention as data first and storage lifecycle as a second line of defense. A bucket rule can remove objects after a configured age, but the business deadline may be per document, and deletion can race with a late upload or a legal hold. Keep the deadline in the application record, make the deletion operation idempotent, and write an audit event for each state transition.

One practical state model is issued, uploaded, verified, held, delete_due, deleted, and deletion_failed. A worker claims due records with a lease, asks storage to delete the exact key, and records the provider response. A missing object should be handled as an already-deleted outcome only when the audit trail can distinguish a prior successful deletion from an unexplained absence. Otherwise, the system quietly loses evidence.

The deadline also needs a clock policy. Store timestamps in UTC, define whether the deadline is inclusive, and test boundary behavior around midnight and daylight-saving changes in the user’s locale. This is unglamorous work. It is also where retention systems become defensible.

How do you select an object storage API for browser direct uploads?

The API should support a server-mediated authorization step and a browser upload that does not expose long-lived storage credentials. The exact transport may be a signed URL, a signed form policy, or a scoped delegation token. Pick based on the constraints you can test, not on the elegance of a client library.

For a short-list comparison, the meaningful differences are these:

Storage family Browser delegation primitive Selection boundary
Amazon S3 Presigned request Strong fit when the application already models object keys, request signatures, and bucket lifecycle rules.
Google Cloud Storage Signed URL Check signing version, request headers, and the operational model for retention and deletion jobs.
Azure Blob Storage Shared access signature Check permission granularity, expiry handling, and how the team will audit delegated access.

All three can be used without giving the browser a general-purpose secret. None of them automatically validates that a PDF contains the expected signature, and none turns a successful upload into a durable business record. Verification, malware scanning, and retention decisions remain application responsibilities.

The trade-off is operational. A signed URL is a poor fit when the application needs a multi-step approval transaction before upload; a broader delegated token may be easier to integrate, but it increases the damage from a leaked browser credential. Provider lifecycle rules are useful backstops, yet they are a poor substitute for per-document deadlines and an auditable deletion worker.

What should the evaluation harness test?

I keep the notebook-to-prod path concrete: the same cases that demonstrate the upload in a notebook should become automated checks against a disposable bucket and a fake clock. The harness should attempt a valid upload, an oversized body, a wrong content type, a reused grant, an expired grant, and an upload to a different object key. It should then advance time past the document deadline and verify both the storage state and the audit record.

Token cost is not the main concern here, but it still affects agent-assisted workflows. If an agent classifies documents before retention is assigned, send it only the metadata and extracted text required for that decision. Keep the authoritative deadline in deterministic application code. A prompt should not be the policy engine.

Observe the full path with correlation IDs that exclude document contents: grant issued, upload acknowledged, verification completed, deletion scheduled, deletion attempted, and deletion confirmed. Alert on stale delete_due records and repeated failures. Do not log signed URLs or delegated tokens; their URLs are credentials until expiry.

A small amount of infrastructure pays for itself when the failure modes are explicit. The right API is the one whose permission boundaries, deletion semantics, and audit signals your team can test and operate consistently.

References

Top comments (0)