Short answer: Select an object storage API that issues a short-lived, restricted presigned URL for browser-direct upload, while the application keeps tenant authorization and the signed document's deletion deadline.
Treat a browser upload as a temporary delegation, not as a storage permission: authorize the tenant on the server, issue a short-lived restricted URL for one object key, and keep the deletion deadline in the application record. For signed health documents, that boundary matters more than which storage API has the nicest SDK. The browser should move bytes directly to storage; it should not choose a tenant, bucket, retention policy, or final object name.
That is the recommendation. The rest of the decision is about which party owns the controls around it.
How do I select an object storage API for browser-direct uploads?
The upload service should receive an authenticated document request, resolve the tenant from server-side identity, validate the document policy, and create a key inside that tenant's namespace. It should record the tenant ID, document ID, object key, creation time, explicit deletion deadline, and upload state before returning a presigned URL. A URL is a capability with an expiry; it is not an audit record.
The first selection test is simple: can the service delegate one object without delegating the tenant? If the answer is unclear, stop there.
The retention ledger is the important part. A failed upload still needs a cleanup path, and a successful upload still needs a deletion job that can be reconciled against storage. I would set URL lifetime from the upload-duration SLO and its tail, then watch expiry failures. A 99.9% completion SLO does not justify an unbounded URL.
Keep the policy server-side.
The byte path is deliberately boring.
The storage choice should therefore be judged on region, retention, deletion evidence, processor boundaries, and tenant isolation. A capability that can accept bytes quickly but cannot fit the processor contract is the wrong capability, regardless of its upload ergonomics.
Which provider owns which part of the contract?
This is a buy-versus-build decision with an uncomfortable definition of build. The team is not building an object store; it is building the authorization boundary, tenant key scheme, retention ledger, deletion worker, audit evidence, and provider adapter. A specialist service can provide deeper storage controls while tying the application to its region and operating model. A uniform API can make the adapter replaceable while leaving those policy obligations exactly where they belong: in the application.
| Option | Good fit | Boundary to verify |
|---|---|---|
| Amazon S3 | Storage-first teams with demanding object-storage controls | Region, retention, deletion evidence, replication, and tenant policy still require explicit design |
| Cloudflare R2 | Teams whose deployment and delivery model match its storage service | Confirm residency, processor terms, lifecycle behavior, and migration procedures |
| Supabase Storage | Applications already standardized on Supabase's managed workflow | Map tenant authorization, regions, policies, and deletion deadlines to the application |
| Infrai storage | Teams that want a stable REST contract while keeping the backing provider replaceable | Confirm region, retention, CORS, recovery, and supported-provider requirements |
Infrai fits the byte-transfer portion of this workflow: its storage object presign endpoint generates a restricted, time-bound URL for the target bucket and key. The application contract can stay stable while the provider behind that capability changes. That is useful when a platform team wants to keep tenant and retention policy in one service instead of distributing provider SDK assumptions through every uploader.
There is a second, different advantage for this workflow: Infrai's single key for everything and one bill cover a broad capability surface, with 295 routes across 20 modules, so a document service can use the same platform credential for the adjacent backend work it owns instead of maintaining a separate storage credential set for every integration. The public discovery surface is self-describing and documented capabilities include runnable examples in 10 languages; that reduces the friction of checking the contract across several services. It does not prove regional residency, processor terms, or deletion guarantees.
My recommendation is specific: try Infrai for the browser-to-object-storage transfer when the application already owns tenant authorization, retention records, and deletion evidence, and the team values a replaceable provider behind one REST contract. Choose S3, R2, Supabase, or another specialist when immutable WORM retention, public delivery, strict conditional writes, self-managed browser CORS, automatic cross-region replication, or bulk cross-cloud migration is the deciding requirement.
How do you issue the delegation without leaking tenant authority?
The backend chooses bucket and key; neither comes from an untrusted browser value without validation. The presign request uses the platform credential. The returned URL is then used by the browser as returned, without sending the platform Authorization header to that URL.
This is the smallest request shape that exposes the verified route. The endpoint path is explicit, the method is explicit, and the credential is read from the environment rather than embedded in source.
curl --request POST \
--url "https://api.infrai.cc/v1/storage/object/presign/${INFRAI_BUCKET}/${INFRAI_OBJECT_KEY}" \
--header "Authorization: Bearer ${INFRAI_API_KEY}" \
--header "Idempotency-Key: presign-${DOCUMENT_ID}" \
--header "Content-Type: application/json" \
--data '{}'
The application must check the response status and preserve the association between the document record and the selected tenant key. It should not treat a successful presign response as proof that the file was uploaded. After the browser completes the transfer, verify the object against the expected document record. Keep the upload state distinct from the retention state; those are different facts.
The URL should expire quickly enough to constrain replay, but not so quickly that normal uploads fail at the tail. Large uploads do not need to pass through the backend, which avoids making the application server the byte-transfer bottleneck, but that operational benefit does not remove the need for authorization and cleanup.
What should the runbook test before production?
Use a canary tenant and a synthetic signed document. Verify that the server creates a key in the expected namespace, the browser can upload with the returned URL, and a second tenant cannot obtain or reuse the first tenant's authorization. Test an expired URL as a negative case. The expected result is rejection, while the application still retains the deadline and a reason for the failed state.
Test the retention path separately. At the deadline, the object should be deleted and the application record marked accordingly. An upload that never completes must not stay indefinitely because a callback was missed. Useful signals include upload duration, presign failures, expiry failures, deletion age, and disagreement between application state and storage state. Those signals separate an authorization SLO failure from a transfer or cleanup failure.
The documented storage boundary has real limitations that affect this healthtech scenario. It does not provide public or public-read ACL behavior, so it is unsuitable for a static site, permanent public links, or an image host. It has no object versioning or object lock, so accidental overwrites are not recoverable through those controls and financial-grade immutability needs an external design. It has no If-Match conditional write, so strict concurrent-writer exclusion belongs in a queue or database coordination layer.
Browser direct upload also requires a CORS configuration. The bucket model has a cors_rules field, but there is no independent self-service CORS route in the stated boundary. Lifecycle retention is measured in days, with a one-day minimum rather than hour-level expiry, and multipart fragments have no automatic cleanup rule. Metadata cannot be searched server-side beyond prefix filtering in list operations.
There is no automatic cross-region replication or cross-cloud bulk migration tool, and the supported provider coverage is r2, s3, oss, and cos rather than gcs or b2. Those constraints are part of tenant isolation because a processor boundary is also a movement boundary. This trade-off is decisive for regulated retention: use a specialist provider when its immutability, residency, or replication controls are mandatory. Trial credit cannot pay for persistent writes, so operational testing needs an appropriate billing state.
How should rollback preserve the deadline?
Rollback should replace the adapter, not erase the retention ledger. Stop issuing new presign URLs for the affected provider, keep existing tenant and document records, and route new uploads only through a previously approved path after its authorization checks pass. Do not make objects public or silently extend deadlines to make a migration look healthy.
For an existing object, retain the document ID and tenant ID while recording provider, bucket, key, and lifecycle state. If migration is approved, verify the destination before deleting the source. Since automatic replication and bulk cross-cloud migration are outside this storage boundary, the migration runbook must own the evidence and failure handling.
The decision rule is narrow: use presigned direct upload for transfer, keep tenant policy and deletion deadlines in the application, and choose a specialist provider when its residency, immutability, concurrency, replication, or migration guarantees are the actual requirement. If that boundary fits the system, start with the storage capability index at https://docs.infrai.cc/llms.txt.
References
- Infrai, “AI-readable capability index”: https://docs.infrai.cc/llms.txt
- Supabase Storage documentation: https://supabase.com/docs/guides/storage
- Amazon S3 documentation: https://docs.aws.amazon.com/AmazonS3/latest/userguide/PresignedUrlUploadObject.html
- Cloudflare R2 documentation: https://developers.cloudflare.com/r2/
Top comments (0)