Dev.to Security πŸ” Cybersecurity πŸ‘ 0 πŸ“– 7 min read

E-commerce Report Delivery: Presigned URLs vs Backend Proxy for Large-File Uploads

Short answer: for an e-commerce web app serving generated reports to authenticated customers, start with a presigned URL and a browser-to-storage byte path. Keep the backend in charge of identity, object naming, and comp

Short answer: for an e-commerce web app serving generated reports to authenticated customers, start with a presigned URL and a browser-to-storage byte path. Keep the backend in charge of identity, object naming, and completion. Use a proxy upload backend when the application must inspect, transform, or approve the bytes before storage accepts them. Measure the complete workflow, because a faster upload can still produce a slower or less trustworthy report-delivery system.

The interesting part is not the URL. It is deciding who owns each transition from β€œthe customer requested a report” to β€œthe customer can download the verified file.”

How should beginners compare presigned URLs and a proxy backend for secure, low-latency uploads?

Imagine a customer exporting a large order report from an e-commerce dashboard. The report is generated by a worker, stored as a private object, and made available through an authenticated download link. The browser may also upload a supporting file, such as a returns spreadsheet, before the report job starts.

With a presigned URL, the authenticated backend creates an application record, chooses an object key, and grants the browser a short-lived permission for one storage operation. The browser sends the bytes directly to storage. The backend then verifies completion and changes the record from pending to ready. The application does not carry the whole file through its own request workers.

With a proxy upload, the browser sends bytes to the backend, and the backend sends them onward to storage. That extra hop is useful when policy requires inline malware scanning, content normalization, or a decision before persistence. It also means another memory, timeout, connection-pool, and bandwidth boundary sits on the large-file path.

That distinction makes the first decision fairly mechanical:

Bytes are the workload.

Requirement Better starting point Reason to reconsider
Large customer files and ordinary private storage Presigned URL The browser-to-storage route still needs careful authorization and completion checks
Inspect or transform bytes before storage Proxy upload backend The application must budget for the extra hop and backpressure
Unstable client networks Either, with resumable behavior where supported Test retries and partial uploads instead of trusting one request timeout
Public, permanent assets A separate delivery design A customer report should not become public merely because sharing is convenient

This is the experiment note I use for notebook-to-prod work: implement both paths behind the same application state machine, feed them the same report-sized files, and compare verified-ready outcomes rather than only time-to-first-byte. I would log p50 and p95 completion latency, bytes crossing the application, retry counts, abandoned pending records, and duplicate report jobs. I would also keep the report generator out of the first comparison. If one path spends 40 seconds uploading and the other spends 15, but the slower path reaches a verified object more consistently under a throttled connection, that difference belongs in the decision. The same goes for the proxy's resource profile: a test that uploads one file at a time can hide the queueing and connection pressure that appears when a sales team exports reports for many stores at once. Start with a fixed file set, fixed regions, and fixed concurrency; record the conditions beside every result so a later notebook can reproduce the comparison. I’m not sure which path will win for your regions and customer connections; those measurements resolve the uncertainty.

The storage path is only half the security boundary

A signed URL is a capability with a narrow scope, not proof that the entire upload flow is authorized. The backend should authenticate the customer before issuing it, create the key under the correct tenant or account namespace, constrain the operation and object identity, and give it a short lifetime appropriate to the workflow. The client should never get a general storage credential.

The completion callback needs equal attention. A browser saying β€œupload finished” is an event to check, not a database fact. The backend should confirm that the expected object exists, belongs to the expected tenant, and matches the application record before it queues report generation or marks the file ready. A repeated callback must be harmless. In an eval harness, I send the callback twice and expect one logical report job, not two.

The proxy path does not remove these obligations. It moves the byte stream through a trusted service, but that service still needs request authentication, maximum-size enforcement, content-type policy, tenant isolation, and an idempotent completion transition. It also has to avoid buffering an entire large file in a web worker when streaming is possible. β€œIt passed through our backend” is not a security design.

Downloads deserve a separate authorization check. A report generated for one store or customer should remain private even if its object key is guessed. Generate access for the authenticated customer at download time, or stream it through an endpoint that checks ownership. Do not make a private report public to avoid designing that check.

A pending record prevents the most expensive AI mistake

For a RAG or agent feature, the file is rarely the final product. It becomes input to parsing, extraction, embedding, or report generation. That makes the application record more important than the storage listing.

Create a pending row before issuing the upload permission. Give it a generated object key, the customer or tenant identity, an expected size if known, and a single logical job identifier. Only after verification should a worker consume it. This creates a clean boundary for retries: an expired URL can create a new attempt for the same logical upload without silently creating a second customer report.

The object key should be an application decision, not a path supplied verbatim by the browser. A safe shape might include an opaque customer identifier, a report identifier, and a generated filename suffix. The exact format is less important than making collisions and cross-tenant writes difficult by construction.

Here is the policy surface I would put in a small Python module before wiring it into a framework handler:

from dataclasses import dataclass


@dataclass(frozen=True)
class UploadDecision:
    inspect_before_storage: bool
    transform_before_storage: bool
    large_report_files: bool


def choose_upload_path(decision: UploadDecision) -> str:
    if decision.inspect_before_storage or decision.transform_before_storage:
        return "proxy"
    if decision.large_report_files:
        return "presigned"
    return "presigned"


decision = UploadDecision(
    inspect_before_storage=False,
    transform_before_storage=False,
    large_report_files=True,
)
assert choose_upload_path(decision) == "presigned"

The function is deliberately boring. The hard work belongs in the policy review and the state transitions, not in clever branching hidden inside an upload controller. My prompt-cost reflex applies here too: do not send a duplicate file into an ingestion pipeline just because an upload callback was retried.

Where the direct path stops being the right fit

The catch is that direct upload is not suitable when the product requirement is β€œreject or rewrite these bytes before they are persisted.” A synchronous content decision, a mandatory transformation, or a network policy that forbids browsers from reaching storage may justify a proxy. Stick with the proxy when that control is non-negotiable, then load-test the service with the largest supported files and the slowest expected clients.

Direct upload is also a poor fit for a design that needs permanent public URLs, immutable retention, or a delivery layer with capabilities beyond private object storage. Those are separate requirements, and a signed upload permission does not solve them. Choose a storage and delivery model that explicitly covers the retention and access contract instead of stretching the upload mechanism into a full publishing system.

Lifecycle rules matter for report systems. Temporary inputs, abandoned multipart work, and generated reports have different retention needs. Define deletion deadlines and audit the state in the application database. The AWS S3 lifecycle documentation and Google Cloud Storage documentation describe provider-side storage behavior, but they do not replace a product policy for when a customer report is considered disposable.

There is a usability trade-off as well. A short-lived permission limits exposure but can expire during a slow upload. A long-lived permission gives a client more time but widens the window. Let the backend issue a fresh attempt after re-authentication rather than making a single token last for the whole life of a report.

What should the production eval measure before choosing a data path?

Run a small matrix, not one laptop demo. Use small and large report inputs, a normal connection, a throttled connection, an expired permission, a cancelled browser tab, and a retried callback. Run each case through the presigned and proxy paths using the same object naming and application state rules.

The assertions should be concrete:

  • An unauthenticated or wrong-tenant customer cannot obtain permission for another tenant’s key.
  • An upload that has not been verified cannot enter report generation.
  • A repeated callback produces one ready record and one queued job.
  • A cancelled upload leaves an observable pending or abandoned state for cleanup.
  • The proxy does not buffer more data than its memory budget allows.
  • The browser sees a useful retry path after an HTTP 403 caused by an expired permission.

For capacity, record browser-observed completion latency, application CPU and memory, application egress bytes, storage completion time, and the percentage of records that reach verified-ready. For cost, separate storage and request charges from application compute and egress; otherwise the β€œcheap” path may only be hiding work in a different bill. Do not use a single average to make this choice. Large-file throughput is a tail problem.

For the e-commerce report scenario, the default is a presigned browser-to-storage path with a strongly owned application state machine. A proxy earns its place when byte-level inspection or transformation is part of the contract. The choice is less about beginner versus expert terminology than about making authorization, throughput, retries, and retention testable before customers depend on the download link.

References

πŸ“° Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.