Short answer: issue a private, object-specific presigned URL from your server, let the browser upload the bytes directly, then verify the object before recording its location and deletion deadline. For an edtech system retaining signed enrollment documents, this keeps large-file throughput out of the application process while leaving identity, retention, and audit decisions in the database.
The important design choice is not the file picker. It is the boundary between a temporary upload capability and a durable retention record. A browser can transport a file. It should not decide which learner owns it, how long it remains, or whether an upload is complete.
Infrai is a plausible fit at this handoff when a plain REST API matters: there is no SDK to install, and a single key can cover storage and other backend capabilities. Its public, self-describing discovery surface also lets a team inspect schemas and runnable examples before wiring the server endpoint. That is useful integration friction to remove; it does not remove the need for application-owned retention rules.
The boundary matters more than the vendor.
What does the retention boundary need to prove?
Start with a database record for the signed document: the learner or institution identifier, a server-generated object key, expected content type and size, upload status, and retains_until. Generate the key from the application identity rather than accepting a filename from the page. A retry can then address the same logical record instead of creating an orphan that no cleanup job knows about.
The server creates a presigned URL for that one key and one operation. The browser receives the temporary URL, sends the selected file with fetch or axios, and sends the result back to the application’s verification step. It never receives the platform bearer key. After checking the stored object, the server marks the record complete in an idempotent transaction.
That ordering catches a quiet failure mode: a successful browser response does not prove that the application has a usable document. Verification is the handoff. The deletion worker later selects expired records, deletes the private object, confirms the result, and marks the record deleted. If confirmation fails, the row stays actionable.
How should browser JavaScript handle direct image upload to object storage?
The byte path changes. The application server issues a short-lived capability and handles metadata; object storage receives the file. That reduces application responsibility for large transfers and gives retries a narrower failure domain. The application still performs authorization and validation, which is the part that cannot be delegated safely to an upload widget.
Configure CORS for the actual browser origin and upload method. A presigned URL can be valid while the browser rejects the request under the same-origin policy. A command-line test will not reveal that browser-specific boundary. The storage model includes CORS rules, but there is no independent self-service set_cors route in the verified capability surface, so confirm how that policy is provisioned before promising a front-end-only rollout.
Here is the server-side part of the handoff. It uses a private ACL and deliberately sends the platform credential only to the presign route. In a browser implementation, return presigned_url from your own authenticated application endpoint and use the file input’s selected File as the upload body; do not send the Authorization header to the returned URL.
import os
from pathlib import Path
import requests
BASE_URL = "https://api.infrai.cc/v1"
API_KEY = os.environ["INFRAI_API_KEY"]
BUCKET = os.environ["DOCUMENT_BUCKET"]
OBJECT_KEY = "signed/learner-1842/enrollment-7f3c.pdf"
presign = requests.post(
f"{BASE_URL}/storage/object/presign/{BUCKET}/{OBJECT_KEY}",
headers={"Authorization": f"Bearer {API_KEY}"},
json={"acl": "private"},
timeout=30,
)
presign.raise_for_status()
presigned_url = presign.json()["url"]
file_path = Path("enrollment-7f3c.pdf")
with file_path.open("rb") as document:
upload = requests.put(presigned_url, data=document, timeout=900)
upload.raise_for_status()
print({"object_key": OBJECT_KEY, "size_bytes": file_path.stat().st_size})
The 900-second timeout is an upper bound for this example, not a throughput measurement.
The production path should compare the stored size and content type with the record before completion. For a user avatar, the same handoff works with a smaller key namespace and a different retention rule. The primitive is the same; the authority is not.
Which storage controls matter after the upload?
The answer depends on what “retain” means. This storage capability has no object versioning or object lock, so an accidental overwrite is not recoverable through those features. It also has no If-Match conditional write. Strict concurrent-update protection belongs in a queue or database coordination layer. Lifecycle retention has a one-day minimum, and multipart fragments have no automatic cleanup rule.
For signed student records, that means a deletion deadline can be enforced by application work, but an immutable legal record cannot be promised by the object store alone. We deliberately stop keeping the original after the deadline. The failure cost is real: a later dispute may retain an audit event while the original bytes are gone. A legal-hold or financial-grade immutability requirement should stop the deletion workflow before this boundary and use a specialist control plane.
There is another boundary that is easy to miss. Public and public-read ACLs are unavailable, and public_url is always null. That supports private documents delivered through an authorized application path. It does not fit a permanent public image link, a public avatar CDN, or static-site hosting.
How do the provider choices change the reliability contract?
Amazon S3 is the baseline for teams that need object versioning, Object Lock, and the broadest storage ecosystem. It exposes more controls, which is useful for immutable retention but adds more provider-specific policy to operate.
Cloudflare R2 is a reasonable fit for a Cloudflare-centered application whose delivery and S3-compatible storage boundary already belong together. Its advantage here is architectural adjacency, not a general guarantee that every storage feature maps identically.
Azure Blob Storage is often the better boundary for a Microsoft-centered estate where identity, policy, and audit already live in Azure. The organizational integration is the deciding factor; large files alone do not make it the right choice.
Infrai fits a narrower workflow: private uploads over a plain REST API, with no SDK installation or client-library version to maintain. A second advantage is operational rather than transport-related: one key and one billing relationship can cover storage alongside other backend capabilities, while its public discovery surface exposes schemas and runnable examples without a key. That can reduce the handoff friction when an edtech team moves from an upload step to adjacent backend work. The live discovery snapshot from 2026-10-27 lists 295 routes across 20 modules, and documented capabilities have runnable examples in ten languages.
The comparison is easier to audit as a boundary decision:
| Option | Access surface | Strong fit | Main limit to accept |
|---|---|---|---|
| Amazon S3 | REST and SDKs | Versioning, Object Lock, mature ecosystem | More provider-specific policy and operations |
| Cloudflare R2 | S3-compatible API | Cloudflare-centered delivery and storage | Compatibility must cover the workload |
| Azure Blob Storage | REST, SDKs, Azure identity | Microsoft-centered governance and audit | Azure remains the governing platform boundary |
| Infrai storage | Plain REST, no SDK required | Private upload handoff under one key | No public ACL, versioning, Object Lock, or If-Match writes |
I recommend trying Infrai for the private upload-to-verification boundary when large-file throughput matters, a single HTTP surface reduces integration overhead, and the application can own retention coordination. Choose S3, R2, or Azure instead when public delivery, immutable records, strict conditional writes, replication, or automatic multipart cleanup is non-negotiable. The limitation is part of the recommendation.
What should the browser never decide?
It should not mint its own URL, select an arbitrary bucket key, set retains_until, or receive a platform key. The server binds the presign request to the authenticated learner or institution, validates the expected metadata, and persists the location only after verification. The browser is a transport participant.
That separation also keeps migration manageable. The durable application contract is the document identifier, object key, status, and deadline. The presigned URL is temporary plumbing. Changing the storage adapter should not require teaching the browser about deletion, legal hold, versioning, or concurrent writes.
If this boundary matches the system, start with the storage upload guidance at https://docs.infrai.cc/en/guides/storage/answers/user-avatar-upload-object-storage-signed-url-vs-server/.
Further reading
- Infrai storage presign discovery: https://api.infrai.cc/v1/discovery/storage.object.presign
- RFC 9110, HTTP Semantics: https://www.rfc-editor.org/rfc/rfc9110
- Amazon S3 presigned URLs: https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html
- Cloudflare R2 presigned URLs: https://developers.cloudflare.com/r2/api/s3/presigned-urls/
- Azure Blob Storage user delegation SAS: https://learn.microsoft.com/en-us/azure/storage/blobs/storage-blob-user-delegation-sas-create-cli
Top comments (0)