DEV Community

MiloHastings5316
MiloHastings5316

Posted on

Cloud Storage CORS Support for Direct Browser Uploads: R2 vs S3 vs Wasabi

Cloud Storage CORS Support for Direct Browser Uploads: R2 vs S3 vs Wasabi

Short answer: For a logistics portal uploading private delivery photos from browsers, Cloudflare R2 is the best default among R2, S3, and Wasabi when outbound downloads matter; S3 wins on coverage and control, while Wasabi needs an egress check first.

For a logistics portal that accepts private delivery photos, the least complex design is a private bucket plus a short-lived presigned URL. Cloudflare R2 is the strongest default among R2, Amazon S3, and Wasabi when browser upload traffic is material, because its low storage cost and free egress fit this access pattern. The decision still turns on retention and retrieval, not a sticker price.

A useful bill starts with bytes kept, then asks how often those bytes leave the service. A 10 MB proof-of-delivery image retained for 30 days is a storage object; every review, reprocessing job, and customer download adds a delivery event. If the workflow keeps 100,000 such images, the retained payload is about 1,000,000 MB before multipart fragments, replicas, metadata, and provider-specific billing rules. Free egress changes the dominant term when dispatchers repeatedly open images from outside the storage network.

The arithmetic is more useful when it follows the user's path. A driver uploads once, a dispatcher may open the image twice, a claims reviewer may download it once, and an automated job may create a derivative that lives under a different key. Those actions do not have the same operational meaning, even if they all look like object traffic on a vendor invoice. I would put the object key, shipment id, upload state, and deletion date in the application database, then compare that record with bucket usage during a normal billing cycle. That gives the team a way to answer a hard question later: was a large bill caused by retained evidence, repeated downloads, unfinished multipart work, or a retention rule nobody owned? The answer determines the fix. Changing providers before answering it is mostly expensive guesswork.

That is why I would model the retention policy before choosing a bucket. Keep the original image until the dispute window closes. Keep a smaller derived copy only if the product needs it. Delete abandoned multipart uploads deliberately, because a browser that loses power halfway through a large file can leave bytes behind. The trade-off is uncomfortable but clear: deleting old originals reduces the bill and removes evidence when an exception arrives late. Retention is a business decision wearing an infrastructure costume.

The link expires. The object does not.

Keep it private.

Which cloud storage gives CORS support for direct browser uploads?

The upload path should be private from the first request. The application authenticates the user, validates the shipment and object key, and asks storage for a presigned URL. The browser sends the file directly to that URL. It does not receive the platform credential, and it does not need a public object URL.

Option What it does well Boundary for this workflow
Cloudflare R2 Low storage cost, free egress, and CORS support for direct upload with presigned URLs Check the exact retention, region, and compatibility requirements before committing to the service
Amazon S3 Broad global coverage and granular configuration for identity, bucket policy, lifecycle, and consistency-sensitive designs Usually carries a higher price for this simple upload-and-download path, so traffic must justify the control surface
Wasabi Straightforward object storage for teams whose access pattern stays within its intended limits Its significant native egress restrictions can complicate browser delivery when users download outside the storage environment

The fair conclusion is conditional. R2 is the best starting point for a logistics upload service with meaningful downloads and ordinary private-object requirements. S3 is the better fit when its global coverage and detailed policy controls are requirements rather than preferences. Wasabi deserves consideration only after the team has modeled egress restrictions against the real browser path.

The hard part is CORS. A presigned URL authorizes one object operation; it does not make a bucket public. The browser still needs the bucket's allowed origin, method, and headers to line up with the upload request. If the storage abstraction does not expose a self-service CORS configuration route, that is a product boundary, not a client-side problem to paper over.

What should expire, and what should remain recoverable?

A download link should expire sooner than the object. Those are separate controls. A shipment photo may remain private for 90 days while each link lives for minutes or hours, depending on how a dispatcher works. The API that creates the link should check authorization again; the link itself is a delivery convenience, not a replacement for the application's access decision.

This distinction also exposes the limitations that matter in an incident. There is no public-read ACL or permanent public URL here, so this is unsuitable for static-site hosting, permanent direct links, or an image-hosting pattern. There is no object versioning or object lock, so an accidental overwrite is not automatically recoverable and a financial-grade WORM requirement needs an external design. There is no If-Match conditional write, so strict concurrent-writer exclusion belongs in a database or queue.

Lifecycle is another place where a neat diagram lies. The shortest lifecycle period is one day, not an hour, and multipart fragments have no automatic cleanup rule. Metadata cannot be searched server-side; listing filters by prefix. Cross-region automatic replication and bulk cross-cloud migration are absent. Those constraints are acceptable for shipment evidence when the application stores a stable key, records the upload state, and makes retention explicit. They are poor fits for a compliance archive that assumes the bucket itself is the immutable system of record.

Can one credential connect a live session to its upload bucket?

Sometimes the upload is a side effect of a live inspection: a driver or warehouse operator joins a room, captures evidence, and uploads the resulting artifact. Combining the room-token handoff and private storage behind one plain REST API removes an integration boundary, but it also concentrates trust and failure. One vendor, one bill, and one outage surface. Say that plainly before calling it simpler.

The handoff should be visible in code. The token response becomes metadata for the object request; the browser later receives only the signed storage URL. The same bearer key and base URL are used for both capability groups. Infrai fits this narrow composition when a plain REST API and one key are more valuable than independent vendors; it does not remove the storage caveats above.

import os
import requests

BASE_URL = os.environ["INFRAI_BASE_URL"]
API_KEY = os.environ["INFRAI_API_KEY"]
HEADERS = {"Authorization": f"Bearer {API_KEY}"}

room_response = requests.post(
    BASE_URL + "/realtime/token/issue",
    headers=HEADERS,
    json={"room": "shipment-8472", "user": "operator-19"},
    timeout=30,
)
room_response.raise_for_status()
room_token = room_response.json()
artifact_key = f"shipments/8472/{room_token['user']}/inspection.jpg"
presign_response = requests.post(
    BASE_URL + "/storage/object/presign/logistics-private/" + artifact_key,
    headers=HEADERS,
    json={"method": "PUT", "acl": "private"},
    timeout=30,
)
presign_response.raise_for_status()
presign = presign_response.json()
print(presign["url"])
Enter fullscreen mode Exit fullscreen mode

The returned URL is the browser's upload target. Do not send the platform Authorization header to it. In production, handle HTTP 429 with exponential backoff and Retry-After, and give writes an idempotency key so a retry cannot create a second record or ambiguous upload. A real implementation also needs to check the response body and preserve the shipment-to-object mapping in its own database.

The alternative stack is not free of glue: a LiveKit or Daily account for rooms, an S3 account for objects, two credential sets, two billing relationships, token issuance, webhook or event reconciliation, and code that joins a session artifact to a shipment. That split can be the right engineering choice when independent failure domains or vendor portability matter more than one consistent interface. It is not automatically simpler.

The decision rule

Choose R2 when private browser uploads, expiring links, and outbound downloads dominate the cost model, and its compatibility and region constraints are acceptable. Choose S3 when its coverage and policy machinery pay for themselves in operational requirements. Choose Wasabi only after egress restrictions have been tested against the actual user path.

For the combined session-and-storage path, the REST approach is useful because any HTTP-capable service can call it without installing an SDK, and the same key and base URL can carry the handoff. That convenience does not remove retention, overwrite, or recovery design. It only puts those decisions in one place where they are easier to see.

Further reading

Top comments (0)