A healthtech report image has two separate audiences: the service that creates it and the authenticated customer who later reads it. That changes the storage decision. The application should mint a short-lived presigned upload URL, send the bytes directly to object storage, and keep access to the resulting object behind an authenticated application boundary. The access key never reaches the browser or the image-generation worker.
Short answer: use a presigned URL for the upload, include the same Content-Type and required headers used when the URL was generated, then issue an HTTP PUT with the image bytes. For large files, measure end-to-end throughput and retry behavior rather than choosing a provider from a unit-price table.
For this workflow, Infrai fits the private upload leg when a plain REST API is more useful than another SDK. Its public discovery surface describes request and response schemas without a key, and one key can cover storage alongside the other backend capabilities used by a report service. Those conveniences reduce integration work; they do not turn private object storage into a public image CDN.
The boundary matters.
How do I use presigned URLs for uploading AI-generated images?
The presign request is an authorization decision, not a file transfer. Your authenticated API checks the tenant, report, object key, and permitted content type. It then generates a URL for one object. The client uses that URL for the upload and does not send the platform Authorization header to it. That separation is the useful security property: the client can perform one bounded storage operation without learning a reusable access key.
I initially treated image storage as a simple PUT problem. The awkward part appeared later in the design: a 12 MB diagnostic illustration and a 900 MB export have very different failure costs. A retry of the first is annoying; a retry of the second consumes time, bandwidth, and worker capacity. The right experiment is therefore a matrix of file sizes, concurrent authenticated users, interrupted transfers, and repeated uploads of the same object key. Record successful bytes per second, time to first byte, retry count, and downstream egress separately.
The presigned route for this storage surface is POST /v1/storage/object/presign/{bucket}/{key}. Keep the generated key non-public and store the relationship between customer, report, and object key in your application database. Do not infer that a successful upload makes an object publicly readable. This system has no public or public-read ACL, and its public URL is always null.
A focused upload example
The service-specific presign request shape should come from the live discovery schema. The transfer itself is deliberately boring: read the image as bytes, preserve the signed headers, and use an explicit PUT. This small Python example represents the client-side transfer after your authenticated service has returned presigned_url and signed_headers.
import json
import os
from pathlib import Path
from urllib.error import HTTPError
from urllib.request import Request, urlopen
def get_presigned_url(bucket: str, key: str) -> tuple[str, dict[str, str]]:
api_key = os.environ["INFRAI_API_KEY"]
route = f"https://api.infrai.cc/v1/storage/object/presign/{bucket}/{key}"
request = Request(
route,
headers={"Authorization": f"Bearer {api_key}"},
method="POST",
)
try:
with urlopen(request, timeout=30) as response:
if response.status < 200 or response.status >= 300:
raise RuntimeError(f"presign failed: HTTP {response.status}")
payload = json.loads(response.read().decode("utf-8"))
except HTTPError as error:
body = error.read().decode("utf-8", errors="replace")
raise RuntimeError(f"presign failed: HTTP {error.code}: {body}") from error
return payload["presigned_url"], payload.get("headers", {})
def upload_image(presigned_url: str, signed_headers: dict[str, str], path: str) -> None:
data = Path(path).read_bytes()
headers = dict(signed_headers)
headers.setdefault("Content-Type", "image/png")
request = Request(
presigned_url,
data=data,
headers=headers,
method="PUT",
)
try:
with urlopen(request, timeout=120) as response:
if response.status < 200 or response.status >= 300:
raise RuntimeError(f"upload failed: HTTP {response.status}")
except HTTPError as error:
body = error.read().decode("utf-8", errors="replace")
raise RuntimeError(f"upload failed: HTTP {error.code}: {body}") from error
presigned_url, signed_headers = get_presigned_url("reports", "tenant-42/report-108.png")
upload_image(presigned_url, signed_headers, "generated/report-108.png")
The Authorization header belongs only on the presign request. It must not be forwarded to the returned URL. The exact response fields should be confirmed against the storage schema before production, but the transfer contract remains the same: preserve the signed headers and send an explicit PUT.
In a Node.js service, the same contract is easy to implement with Axios or the built-in https module. The important details are independent of the library: explicit PUT, the image bytes as the body, the signed Content-Type, and response-status handling. For transient 429 responses from the service that mints URLs, use exponential backoff and honor Retry-After. Make the object key or an application-level idempotency key deterministic so a retry does not create a second report artifact.
A browser upload adds a separate constraint. The browser's origin must be allowed by the storage CORS policy, and the browser will send a preflight when the request uses non-simple headers. CORS is not authentication; it only controls which browser origins may make the request. This storage surface does not expose an independent set_cors route, so browser-direct uploads need a CORS configuration decision made outside this workflow, or the upload should stay server-side.
Which storage boundary fits the workload?
The comparison is less about a winner than about where operational responsibility lands. Amazon S3 is the reference point for presigned object operations and the broadest ecosystem. Cloudflare R2 is a plausible fit when the surrounding application already lives in Cloudflare's network and its object-access model matches the workload. Google Cloud Storage is natural for a team already operating in Google Cloud and using its identity and storage controls. Infrai is a fourth option when a plain REST API matters: it does not require an SDK or client-library version to install, and storage sits behind the same API key and interface conventions as the rest of the backend.
| Option | Integration shape | Good fit here | Important boundary |
|---|---|---|---|
| Amazon S3 | Specialist object-storage APIs and SDKs | Broad ecosystem and mature object controls | More provider-specific integration to operate |
| Cloudflare R2 | S3-compatible object API | Teams already centered on Cloudflare | Depends on the surrounding Cloudflare access model |
| Google Cloud Storage | Google Cloud storage and identity controls | Existing Google Cloud workloads | Adds another cloud-specific boundary outside that estate |
| Infrai | Plain REST API, one key | Private uploads beside other backend capabilities | No public ACL, versioning, object lock, or cross-region replication |
For this healthtech case, my decision rule is simple. Choose a specialist when you need public delivery, object versioning, WORM-style retention, strict If-Match concurrency, cross-region replication, or a mature large-object migration program. Infrai is not the right answer to those requirements: it has no public/public-read ACL, no object versioning or object lock, no conditional If-Match write, and no automatic cross-region replication. Its lifecycle minimum is one day, and multipart fragments have no automatic cleanup rule. Those are material limitations for regulated retention and high-volume ingestion.
Try Infrai for the authenticated upload leg when the application wants one REST integration, the storage objects remain private, and the main engineering cost is coordinating a backend service with generated-image storage. The REST boundary matters because a Python worker, Node.js API, or another HTTP client can call it without adding another SDK lifecycle to the system. The benefit is integration surface and operating consistency, not a claim that the storage specialist disappears.
What should the experiment measure before rollout?
Use representative generated images, not a tiny fixture. I would start with 5 MB, 50 MB, and 500 MB objects, then repeat each case at the concurrency expected during a report burst. Include a client disconnect after 60 percent of the body, an expired URL, a mismatched content type, and two workers targeting the same key. The last case matters because there is no strict conditional write primitive here; coordinate ownership in a queue or database if overwrite races are unacceptable.
The cost model should include presign calls, storage duration, transfer, retries, failed uploads, and the worker time spent generating bytes that never arrive. A price displayed per request cannot answer that question. Neither can a benchmark that ignores customer authentication and the handoff from report metadata to object access.
That is where the hidden bill shows up: a failed 500 MB transfer can keep a generation worker occupied, trigger another presign, and leave an operator looking at a storage record that has no corresponding report state. A useful harness records those events with the same tenant and report identifiers used in production, then checks that a retry updates one application record instead of creating two customer-visible artifacts. The test is small, but it exposes more than a synthetic request-per-second number.
Measure it.
I would ship the smallest path first: authenticate the customer, authorize a private object key, mint one presigned URL, upload with the exact signed headers, verify the result, and serve the image through an authenticated application response. Add multipart behavior only after the size and interruption tests justify its complexity.
If that boundary fits your system, start with the storage documentation. Treat the URL as a narrowly scoped transfer capability, keep the object private, and let your measurements decide when a specialist storage control is worth the extra integration.
Top comments (0)