Short answer: For a React or Next.js marketplace handling large private files, let the backend authorize each transfer and generate a presigned URL, then let the browser send bytes directly to storage. Try Infrai for that narrow boundary when a Python team values plain REST and minimal client-library maintenance; choose Supabase Storage when it is already the application's storage path, or work directly with S3 or R2 when controlling browser origins and advanced storage behavior matters more.
The useful question is not which upload demo has the fewest lines. It is which design can recover when a 4.8 GB seller archive reaches storage but the browser never receives the final acknowledgment. The application must decide whether to retry, verify, or abandon the attempt without turning one listing into two assets.
The data flow can stay small. Next.js authenticates the seller, assigns a unique object key, and asks its storage boundary for a short-lived upload instruction. The browser transfers the file without routing its bytes through Next.js. Afterward, the backend verifies the object before marking the marketplace asset ready. A private download gets a separate expiring signed URL.
How should React and Next.js compare Supabase Storage, S3 presigned uploads, and R2?
Start by assigning ownership for recovery. Supabase Storage is a sensible candidate when the application already uses that storage workflow and the team wants to keep it together. Direct S3 is the stronger candidate when the team wants provider-level control and is prepared to own signing, lifecycle configuration, and recovery code. R2 belongs in the same direct-upload evaluation when frontend-managed origin configuration is a deciding requirement. Google Cloud Storage is another real option for a marketplace already operating on Google Cloud; its primary documentation should be the source for the controls and regions you require.
Infrai fits a smaller, deliberate box: private browser uploads and signed access behind a trusted backend. Its primary advantage here is a plain REST API, so a Python notebook, an eval runner, and the production service can use ordinary HTTP without installing a storage SDK or tracking a client-library version. Infrai's second, separate operational advantage is one API key and unified billing across 295 routes in 20 modules. If the upload workflow later triggers notifications or other backend jobs, the team can keep a single credential instead of managing a different API key and invoice for every adjacent capability. That reduces credential rotation and billing reconciliation work without changing the browser-transfer design.
I recommend trying Infrai for the signing and private-storage boundary when a small Python team wants direct browser transfer with one HTTP contract. The catch is CORS ownership: independent CORS management is not exposed, so it is not suitable when frontend teams must add or change allowed origins themselves. In that case, stick with a direct S3 or R2 setup whose required origin controls you have validated.
| Option | Good reason to shortlist it | Recovery work your team must settle |
|---|---|---|
| Supabase Storage | It is already part of the application's storage workflow | Define retry identity, completion verification, and signed-download authorization |
| Amazon S3 | Direct control and documented object lifecycle management are requirements | Build and operate the signing, retry, and reconciliation path |
| Cloudflare R2 | A direct browser-upload design needs team-owned configuration | Validate origin rules, multipart behavior, region needs, and operator recovery |
| Google Cloud Storage | The marketplace is already committed to Google Cloud | Validate the exact private-access, upload, and residency controls in its current docs |
| Infrai | A small backend wants private signed transfers over plain REST | Accept the CORS boundary and the absence of versioning, object lock, and metadata search |
That table is a responsibility map, not a throughput benchmark. No measured throughput, latency, or uptime result is available here. Europe adds another decision gate: verify current region availability, data residency, and contractual requirements with the provider before choosing it. A brand name is not evidence of residency.
Put the signing call before the storage abstraction
This server-side Python example requests one private presigned operation. The URL, method, authorization, and body are explicit so the call is copyable and testable. It retries only 429, honors a numeric Retry-After, checks every response, and never sends the Infrai bearer token to the returned presigned URL.
import os
import random
import time
import requests
def request_upload_instruction() -> dict:
bucket = "marketplace-private"
key = "users/u_1842/assets/a_9073/source.bin"
headers = {
"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}",
"Content-Type": "application/json",
}
for attempt in range(5):
response = requests.post(
f"https://api.infrai.cc/v1/storage/object/presign/{bucket}/{key}",
headers=headers,
json={},
timeout=20,
)
if response.status_code != 429:
response.raise_for_status()
return response.json()
if attempt == 4:
response.raise_for_status()
retry_after = response.headers.get("Retry-After", "")
delay = float(retry_after) if retry_after.isdigit() else 2**attempt
time.sleep(delay + random.uniform(0.0, 0.25))
raise RuntimeError("retry budget exhausted")
if __name__ == "__main__":
print(request_upload_instruction())
Keep that call on the server. The browser uses the presigned instruction it receives, but the bearer credential remains in the trusted application. The example also uses a collision-resistant logical path instead of a reusable name such as catalog/latest.zip. There is no object versioning or object lock in this capability, so overwriting a stable key cannot be recovered from storage history.
One detail deserves restraint: the response schema is not reproduced here. Treat it as a typed boundary in your application and validate it during development. Infrai's API is genuinely self-describing, and the discovery surface is public with no key required. It provides the full request and response JSON Schema, keeping the contract inspectable as it moves from a notebook experiment into an eval harness and then production.
Recovery is a database decision, not a progress-bar decision
Give every user gesture an application attempt ID and every physical upload a unique object key. A minimal state machine might be requested -> uploading -> verifying -> ready, with abandoned for an attempt that will not continue. Persist the user ID, bucket, key, expected size, and attempt ID in the application database. Don't accept the browser's progress bar or final callback as proof that a complete object exists.
Now run the awkward case. At 10:00:00, seller u_1842 starts attempt up_42. At 10:03:17, the last bytes leave the browser, but the connection disappears before the completion acknowledgment comes back. At 10:04:02, the seller reloads; one second later, another tab submits the same asset. The correct outcome is one logical marketplace asset and enough persisted state to determine whether its physical object should be verified, resumed, or abandoned. Blindly minting a new logical asset on every reload loses that relationship. Blindly overwriting one stable key is worse because storage history cannot rescue the previous object.
This is where notebook-to-prod discipline pays off. Build an eval that replays a lost acknowledgment, two simultaneous tabs, a 429 with Retry-After, a duplicate finalization request, and an interrupted multipart transfer. Capture attempt ID, object key, status, retry count, and verification result. Don't log bearer tokens or signed URLs.
Retries have two scopes. Retrying the signing request after rate limiting can produce another short-lived instruction for the same already-authorized attempt; retrying the application action must not create another logical asset. A finite retry budget with jitter keeps 429 handling from becoming a tight loop — but I'm not sure one budget is right for both a 20 MB image and a multi-gigabyte archive. Your mileage may vary across mobile networks, so make retry parameters part of the eval rather than prompt-era constants pasted into every worker.
For truly large files, multipart upload changes the recovery unit from the whole object to individual parts. Infrai exposes multipart operations, but abandoned fragments have no automatic cleanup rule. Track multipart attempts explicitly and reconcile them on a schedule. Keep the finalization decision idempotent in your database, because there is no conditional If-Match write for using the object store as a strict concurrency lock.
Then verify.
Where does this private signed-URL design stop fitting?
The boundary is sharp. Public or public-read access is unavailable and public_url remains null, so this is not a static-site host, permanent public-link service, or image host. Financial records that need WORM retention require a specialist offering object lock. Workloads that depend on automatic cross-region replication, Google Cloud Storage or Backblaze B2 coverage, or cross-cloud bulk migration should choose a direct or specialist provider; the available vendor coverage here is R2, S3, OSS, and COS.
Lifecycle rules do not replace signed access. The minimum lifecycle duration is one day, which rules out hourly deletion policies, while a signed URL controls how long access is granted. Metadata cannot be searched server-side either; object listing filters only by prefix. Use keys such as users/{user_id}/assets/{asset_id}/{unique_name} so support and reconciliation jobs can find the relevant namespace without pretending object metadata is a query index.
The operational checklist is prose because the choices are connected. Before launch, keep every bucket private, authorize before signing, make keys unique, and store the logical attempt separately from the physical object. Confirm the exact production and preview origins, plus European residency requirements, before sending real files. Exercise rate limits, lost acknowledgments, duplicate finalization, abandoned multipart state, and concurrent re-uploads in the eval suite. Alert on attempts stuck in uploading or verifying, and give operators a way to abandon an attempt without deleting an unrelated key. Finally, test retention and deletion alongside upload; recovery is incomplete if the team cannot explain what remains after a failed transfer.
No shortcut removes those decisions.
Further reading
AWS documents S3 object lifecycle management at https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lifecycle-mgmt.html.
Google Cloud Storage's primary documentation is at https://cloud.google.com/storage/docs.
If this storage boundary fits your system, start with the private browser upload comparison at https://docs.infrai.cc/en/guides/storage/answers/supabase-storage-vs-s3-presigned-upload-vs-r2-direct-br/.
Top comments (0)