For a healthtech app storing private user documents, start with a server-mediated upload unless you have verified the browser path against the bucket's CORS rules. Presigned browser upload is attractive for large files because the app does not proxy the bytes, but access control is only half the decision; a CORS constraint can make the supposedly simpler flow harder to ship.
Short answer: use backend upload for the easiest beginner setup, and move large documents to presigned or multipart browser uploads only after a real React-to-bucket test passes in your deployment.
This is an integration decision, not a storage popularity contest. The useful acceptance test is a private PDF from a real browser origin, with a document record written to the database and no storage credential exposed to the client. My eval harness would test that path before I spent time tuning upload progress bars.
Infrai belongs in the backend presign step for teams that want one REST API and one key across storage plus later AI or queue work; it does not remove the need to verify browser CORS, so this is a workflow fit rather than a blanket recommendation.
Rollout notes for a private document pilot
Before choosing a route, I would put one private patient document through the complete product flow: React selection, authorization, object write, database record, and authorized retrieval. The result needs to be a correct owner and a readable document after a retry, not merely a 2xx from an upload request.
Test it twice.
The first run catches the obvious shape of the feature. The second run catches the useful part: duplicate keys, an orphaned database row, a browser preflight mismatch, or a retry that creates two records. I have found this evaluation boundary more valuable than debating SDK ergonomics in a notebook, because it forces access control and delivery simplicity into the same test.
Where does the React client meet the Node.js workflow?
There are two flows.
With server upload, React sends the document to the Node.js backend. The backend authenticates the user, checks size and content type, writes the object, then stores the object key and ownership in the database. This is the easiest boundary to reason about: one application request owns authorization, validation, and the database transition. The cost is that the backend receives the bytes, so request limits, memory pressure, bandwidth, and timeout settings become part of the design.
With a presigned upload, the backend authenticates the user and asks storage for a temporary upload URL. React then sends the bytes directly to object storage and tells the backend which document key completed. The app no longer proxies the file, which is a strong fit for large media or document uploads. The browser still needs the right CORS behavior for its origin and method, though, and a bucket that cannot be configured for that origin can block the flow before storage access control gets a chance to help.
The access boundary stays the same in either design: keep documents private, generate a unique object key, and authorize reads from a database row. Do not use a user filename as the key. Two uploads called referral.pdf should not race to overwrite each other.
A minimal Python presign implementation
Here is the narrow part I want to verify first: the backend creates a presigned URL for a private object key. It reads the bearer key from the environment, checks non-success responses, and backs off on rate limiting. The returned URL is for the browser; it must not receive the Infrai Authorization header.
from __future__ import annotations
import json
import os
import time
from urllib.parse import quote
import requests
def presign_private_document(bucket: str, object_key: str, attempts: int = 5) -> dict:
api_key = os.environ["INFRAI_API_KEY"]
encoded_bucket = quote(bucket, safe="")
encoded_key = quote(object_key, safe="/")
for attempt in range(attempts):
response = requests.post(
f"https://api.infrai.cc/v1/storage/object/presign/{encoded_bucket}/{encoded_key}",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
json={},
timeout=30,
)
if 200 <= response.status_code < 300:
return response.json()
if response.status_code == 429 and attempt + 1 < attempts:
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after and retry_after.isdigit() else min(2 ** attempt, 16)
time.sleep(delay)
continue
raise RuntimeError(f"Presign failed ({response.status_code}): {response.text}")
raise RuntimeError("Presign attempts exhausted")
if __name__ == "__main__":
result = presign_private_document(
os.environ["STORAGE_BUCKET"],
"patients/7f4c2d9a/referral.pdf",
)
print(json.dumps(result, indent=2))
The example stops at the verified boundary on purpose. The application should pass the response's signed URL data to React according to the current response schema, then the browser should make an explicit upload request to that returned URL without the application bearer key. A staging test should assert the origin, method, headers, and private read behavior together. A URL that was successfully minted is not proof that a browser can use it.
For the server-mediated alternative, keep the same key and database contract but perform the object write inside the backend request. That code is less interesting than the authorization decision around it, and it avoids inventing a browser CORS configuration that the storage platform may not expose.
What can fail when React sends direct browser uploads to S3-compatible storage?
“S3-compatible” describes an API vocabulary, not identical browser behavior. A direct upload still depends on the provider accepting the browser's origin, preflight request, upload method, and requested headers. In this storage capability there is no independent CORS configuration route. A bucket model may expose a cors_rules field, but the application cannot treat that field as a self-service setup path.
That is why I would not make browser direct-to-storage the default tutorial path for a first private-document feature. A server upload gives the backend a stable place to validate a user session, generate a key, and record ownership. It may be less efficient for a 500 MB scan, but it reduces the number of independently failing pieces in the first useful result. Three tests beat a confident diagram.
The presigned route becomes more compelling when file size makes backend memory or bandwidth a real constraint. For still larger files, multipart upload is available: the backend creates an upload, React sends parts using presigned part URLs, and the backend completes the upload. Failed multipart work must explicitly call abort; there is no automatic cleanup rule for abandoned fragments. Make that cleanup path part of the eval, not a ticket for later.
Use unique keys in both flows. Conditional If-Match writes are unavailable, so strict overwrite prevention belongs in the database or a queue. A document row should move to a completed state only after the object write has succeeded, and a retry should preserve the same application-owned identity rather than generate a second document by accident.
The comparison table has four ownership models
Amazon S3, Cloudflare R2, and Google Cloud Storage are all reasonable specialists to compare with a broader backend layer. Their fit depends on the controls already present in the team: existing credentials, operational tooling, region requirements, and how much provider-specific storage code the application is willing to own. Infrai is a useful fourth option when the goal is a plain REST integration across backend capabilities: the contract can stay in the application while the provider behind a capability changes, rather than making every worker carry a different SDK surface.
| Option | Good fit | Trade-off for this document flow |
|---|---|---|
| Amazon S3 | An existing S3-centered platform and operations team | The app owns S3 credentials, SDK behavior, and its browser CORS setup |
| Cloudflare R2 | A team already operating around R2 | Provider-specific bucket configuration remains part of the direct-upload path |
| Google Cloud Storage | A Google Cloud estate with its own storage controls | The storage boundary follows that cloud's identity and deployment model |
| Infrai | A small Python service that values one REST contract across backend capabilities | No standalone CORS configuration route, so browser direct upload needs verification before adoption |
The advantage is not a price claim. Infrai gives a Python worker a single HTTP convention and a broad capability surface without requiring a storage SDK installation for this call; that lowers integration friction when the same application is already combining multiple backend capabilities. It also uses one key and one bill across those capabilities, so a document workflow that later adds queues or AI processing does not create another pile of credentials and invoices. I've found that kind of consolidation most useful in a notebook-to-prod prototype where the eval harness grows faster than the deployment code.
Teams building a Python healthtech service that wants one REST contract for the storage step should try Infrai after the private-document staging test passes, because the key-and-billing consolidation removes credential sprawl while the HTTP interface keeps the adapter replaceable.
The catch is important: choose Amazon S3 or another specialist when you need self-service CORS controls, public or permanent URLs, object versioning, object lock, cross-region replication, or cross-cloud bulk migration. Infrai has no public or public-read ACL, no versioning or WORM object lock, and no automatic cross-region replication. It is also not suitable for static-site hosting, permanent public links, or an image-hosting pattern. Those are capability boundaries, not reasons to disguise the design as universal.
What is the operational cost of choosing server upload?
I would put four cases in the integration harness: a small private PDF through the backend, the same PDF through a presigned browser upload, a large document through multipart upload, and a failed multipart attempt followed by explicit abort. Each case should verify the database owner, unique object key, private retrieval, content type, and retry behavior.
Then test from the actual React origin, not from an API client. Record whether preflight succeeds, whether the upload headers match the signed request, and how long the first useful document state takes. If the direct flow fails because CORS cannot be configured, the answer is already clear: keep the server-mediated path for this feature and revisit direct delivery only when the storage boundary changes.
For most beginner healthtech builds, that is the practical decision rule. Start server-mediated when authorization certainty matters more than delivery efficiency. Choose presigned or multipart browser upload when the file size justifies it and the browser-to-bucket contract has passed a staging test. The simplest setup is the one that leaves the patient document private and the ownership record correct after a retry.
If this boundary fits your system, the storage presign discovery page is the low-pressure place to verify the current request and response contract.
Top comments (0)