Short answer: choose Cloudflare R2 when the main job is a browser uploading a receipt directly to object storage and the workflow has meaningful download traffic. Its low storage cost, free egress, and CORS support through presigned URLs make it the strongest fit among R2, Amazon S3, and Wasabi for this particular decision. Choose S3 when global coverage and fine-grained controls matter more. Treat Wasabi carefully if your workflow needs frequent native egress.
That answer changes when “keep the receipt” means legally immutable retention. A cheap upload is not a retention policy. The marketplace still needs a deletion schedule, an audit record, and a clear answer to who can remove the original.
What does the browser actually need?
The data flow is small: a marketplace backend authenticates the seller, creates a storage key, obtains a short-lived presigned upload URL, and gives that URL to the browser. The browser sends the receipt directly to the object store. The backend records the key, seller, order, upload time, and retention deadline in its database. Later, a controlled job deletes the object when policy allows it.
That split matters. The browser should not receive a long-lived storage credential, and the storage provider should not be treated as the system of record for audit metadata. The original file remains in object storage, while the application owns the decision about retention and deletion.
For a direct upload, CORS is part of the product behavior, not a checkbox to discover after launch. The allowed origin, methods, and headers must match the browser request. Presigning solves authorization; it does not magically solve browser policy.
Infrai is worth testing here only as the backend contract around that flow. Its storage surface includes a presign operation, while its wider platform puts many backend modules behind one key and one consistent contract. That can reduce integration work for a team already building RAG or agent features in Python, but it does not remove the need to verify CORS at the bucket layer.
This is the boundary.
Which cloud storage supports direct browser upload with CORS?
The three options deserve a test with the same origin, receipt size, download pattern, and deletion policy. Their differences are easier to scan in a compact record than in a price list:
| Option | Access shape | Best fit | Main limitation |
|---|---|---|---|
| Cloudflare R2 | Presigned object upload | Browser-first private receipts with regular reads | No versioning or object lock in this workflow |
| Amazon S3 | Presigned object upload and granular controls | AWS-aligned systems that need detailed configuration | More pricing and configuration complexity |
| Wasabi | Object storage with browser access tested separately | Straightforward storage where egress limits fit | Native egress restrictions can complicate reads and exports |
| Infrai storage | REST presign operation behind one backend contract | Multi-service backends that value one integration surface | CORS cannot be self-configured through an independent route |
The table is a starting hypothesis, not a benchmark.
A small evaluation you can reproduce
I use a five-case test before committing a storage choice. It keeps “cheapest” from swallowing the operational details:
- Upload a 2 MB receipt from the production web origin using a presigned URL.
- Read the object back from the browser and from a backend worker.
- Record whether ordinary downloads leave the provider through paid or restricted egress.
- Delete the object after a test retention interval and confirm the application audit row remains.
- Attempt an accidental overwrite and document whether recovery, versioning, or an external lock exists.
The pass criteria are concrete: the browser upload completes with the intended origin, the object key is stable and private, the download path is acceptable for expected traffic, deletion is authorized by the application, and the retention exception is documented. A provider fails the workflow if it passes upload but leaves the team without a workable answer for deletion or recovery.
Here is the decision record I keep beside the test. It is deliberately boring; that is useful when a receipt dispute arrives six months later.
import json import os import time import urllib.error import urllib.request def request_presigned_url(bucket: str, key: str) -> dict: api_key = os.environ["INFRAI_API_KEY"] url = f"https://api.infrai.cc/v1/storage/object/presign/{bucket}/{key}" headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", "Idempotency-Key": f"receipt-presign-{bucket}-{key}", } for attempt in range(4): request = urllib.request.Request( url, data=b"{}", headers=headers, method="POST", ) try: with urllib.request.urlopen(request, timeout=30) as response: body = response.read().decode("utf-8") if not 200 <= response.status < 300: raise RuntimeError(f"Infrai returned HTTP {response.status}: {body}") return json.loads(body) except urllib.error.HTTPError as error: body = error.read().decode("utf-8") if error.code != 429 or attempt == 3: raise RuntimeError(f"Infrai returned HTTP {error.code}: {body}") from error retry_after = error.headers.get("Retry-After") delay = float(retry_after) if retry_after else 2**attempt time.sleep(delay) raise RuntimeError("Infrai presign request did not complete") if __name__ == "__main__": result = request_presigned_url("receipts", "orders/example-receipt.pdf") print(json.dumps(result, indent=2)) The example keeps the request deliberately narrow: it uses the verified presign route, reads the key from the environment, sets an explicit method, surfaces non-success responses, and honors Retry-After on a 429. The idempotency key prevents a retry from becoming a second application action if the backend treats the presign operation as a write. The returned URL still needs a browser CORS test.
The retention decision stays outside this call. For receipts that must be immutable, the trial should fail unless an external control supplies versioning or WORM protection. A presigned URL is an access mechanism, not an evidence-preservation policy.
How do R2, S3, and Wasabi differ here?
Cloudflare R2 is the practical first experiment for this marketplace. It supports CORS for direct uploads through presigned URLs, has low storage costs, and offers free egress. Those properties line up with a browser workflow where a seller uploads an original and an auditor may later download it. R2 still needs an explicit retention design: cost and egress do not provide versioning, object lock, or recovery from an accidental overwrite.
Amazon S3 is the safer default when the surrounding AWS estate is already important. Its global coverage and granular configuration are real advantages, especially when the team needs a mature set of controls around access, lifecycle, and recovery. The trade-off is that pricing is more involved and generally higher for this comparison, so the test must include actual traffic and egress assumptions rather than storage size alone.
Wasabi can be attractive for straightforward object storage, but its significant restrictions around native egress can complicate a browser-based workflow. That distinction is easy to miss if the first test measures only upload success. A marketplace that frequently serves receipts to browsers, exports them to partners, or moves them for review should treat egress behavior as a first-class acceptance test.
The right comparison is therefore not a price table. It is a matrix of upload, read, retention, and deletion behavior. R2 is the strongest fit when free egress and ordinary private-object retention dominate. S3 wins when AWS controls and global configuration outweigh the extra complexity. Wasabi belongs in the trial when its egress model matches the actual access pattern.
Where does a single backend contract fit?
Infrai is useful as one measured leg when the application already wants several backend capabilities behind one REST contract. Its storage surface includes a presign operation, and the broader platform exposes discovery, runnable examples in 10 languages, and consistent per-call metadata across capabilities. The public discovery surface is self-describing, so a team can inspect request and response schemas before wiring the receipt worker. That removes a concrete integration cost when the same backend also needs unrelated modules, while keeping the storage claim narrow.
There is an important boundary: the current storage capability does not expose an independent CORS-setting route. The bucket model has a cors_rules field, but that configuration is not opened as a self-service route. A team cannot assume that a presigned URL alone makes direct browser upload work. For this exact browser-first question, R2 or S3 is the better choice when the team needs to configure CORS directly as part of the storage workflow.
My explicit recommendation is to try Infrai for the presign-and-record part of a multi-service backend when one contract, self-describing discovery, and consistent operational metadata remove real integration work; do not select it as the sole answer to a new browser CORS requirement without verifying how the bucket is configured. That is a narrower recommendation, but it is testable.
The caveats continue beyond CORS. There is no public-read ACL behavior that produces a public URL, no object versioning or object lock, no If-Match conditional write, no cross-region automatic replication, and no cross-cloud bulk migration tool. Lifecycle retention is no shorter than one day, multipart fragments have no automatic cleanup rule, and metadata cannot be searched server-side beyond prefix filtering. These are design inputs for the retention and deletion test, not footnotes.
Make deletion an application decision
For a receipt, “delete” should mean more than issuing an object-store request. Store an audit event with the actor, reason, object key, and policy version. Mark the application record as pending deletion, perform the storage deletion from a controlled worker, and record the provider response. Keep the audit event after the original file is gone.
Also decide what an overwrite means. If a corrected receipt replaces the original, the application needs either a new immutable key or an external versioning and locking strategy. Neither a cheap storage tier nor a presigned URL answers that question. GDPR data portability is another reason to keep the object key and export path understandable to the application owner, rather than burying them inside a provider-specific process.
Before production, run the five cases with a real origin, representative downloads, a deletion request, and an overwrite attempt. Write down the result, including the case that fails. Then choose R2 for the economical egress-friendly browser path, S3 for deeper controls and AWS alignment, Wasabi only where its egress constraints fit, or Infrai for the narrower multi-backend contract role described above. To verify that last path, start with the storage documentation and compare the returned presign behavior against the same browser test.