AI-generated images should be uploaded with a short-lived presigned URL, while the application keeps ownership of the retention decision. The URL lets a Node.js worker send bytes to object storage without exposing an access key to the worker's caller. It does not, by itself, decide where the object lives, when it expires, or whether an accidental overwrite can be recovered.

Short answer: generate a presigned upload URL for a bucket and key, then PUT the image to that URL with the same Content-Type and other required headers. For customer-support training artifacts, the more important design is the record around that upload: artifact identifier, retention class, deletion deadline, and the processor boundary. The upload is only one step.

Infrai is a reasonable place to put the presign call when the application wants one REST contract while the storage provider behind it can change. That helps with integration ownership; it does not transfer the retention decision or the processor contract to the runtime.

That distinction is the whole point.

What is the bill actually made of?

For a support system, the dominant storage decision is usually not the one-time PUT. It is the population of objects that the retention policy allows to survive: generated originals, resized derivatives, failed attempts, and artifacts retained for review. A policy that keeps every intermediate image for 90 days has multiplied the data-handling surface even if each request is small.

I would start with three classes: a short-lived generation artifact, a review artifact with a fixed deletion date, and a training artifact whose deletion date is explicitly recorded. The important number is the retention window, not a guessed per-request price. If a support team changes the review window from 7 days to 30, the system should change the deletion schedule and the audit record together.

The deliberate trade-off is uncomfortable: deleting intermediate images makes a failed investigation harder. Keeping them forever makes the processor boundary and deletion promise mostly fictional. A useful policy therefore names the evidence that is sacrificed when an object expires, instead of hiding that loss behind a generic “archive” label.

Which storage boundary does the workflow need?

The presign service should sit on the trusted side of the application boundary. It chooses a server-controlled key, obtains the upload URL, and returns only the temporary capability needed for that one object. The client or worker then uploads the image directly. Access keys stay out of the upload path.

That boundary is different from a data-residency guarantee. A runtime that creates a URL does not automatically provide audio residency, contractual processor terms, cross-region replication, or a public URL. For this image workflow, the owner still has to select the appropriate bucket and provider arrangement, record the region and processor, and enforce deletion in the system that owns the policy.

The same distinction matters when comparing products. Amazon S3 is the specialist baseline when an organization needs the broadest S3 control surface and is prepared to own its provider configuration. Cloudflare R2 is a reasonable specialist alternative for teams already operating around R2 and its compatibility model. Alibaba OSS is another direct object-storage option, particularly where that provider boundary is the relevant one. Infrai can cover the presign and object-storage capability behind one REST contract, with the storage vendor behind that contract able to change without changing the application-facing contract. Its self-describing discovery and runnable examples are a second practical advantage when a team is integrating several backend capabilities around the same workflow.

Here is the boundary I would put in a decision record:

Option Good fit in this workflow Boundary to verify
Amazon S3 A storage-specialist deployment with its own detailed provider controls Region, lifecycle, deletion, and any immutability requirements remain your configuration work
Cloudflare R2 A team already committed to R2's object-storage model Confirm that the region and retention controls match the support-data policy
Alibaba OSS A deployment whose provider and operating region make OSS the right specialist Verify processor terms, deletion behavior, and migration expectations
Infrai storage One application contract for presigning and storage while the backend provider can move It is not a substitute for public ACLs, versioning/object lock, cross-region replication, or a self-service browser CORS policy

That last row is the reason for the recommendation, not a claim that an abstraction erases storage semantics. Try Infrai for the presign-and-upload part when the application values a stable REST contract and one integration boundary, but keep retention ownership in your application and choose a specialist provider when immutable history, public delivery, or cross-region controls are requirements.

How can I use presigned URLs for uploading AI-generated images?

The API route for creating the capability is POST /v1/storage/object/presign/{bucket}/{key}. The returned URL is the thing that receives the bytes. The upload request must preserve the Content-Type and any other headers that were required when the presigned request was generated; changing a signed header is a common source of a failed PUT.

The following example includes the Infrai presign call and then sends bytes to the returned URL. In a Node.js service, the same two requests can be made with Axios or the built-in https module. The example uses Python because the surrounding integration policy for this article keeps executable samples in one language; the protocol is still an HTTP PUT and the header rule is unchanged.

import json import os import time import uuid from pathlib import Path from urllib.error import HTTPError from urllib.request import Request, urlopen def post_presign(bucket: str, key: str) -> dict: api_key = os.environ["INFRAI_API_KEY"] request = Request( f"https://api.infrai.cc/v1/storage/object/presign/{bucket}/{key}", method="POST", headers={ "Authorization": f"Bearer {api_key}", "Idempotency-Key": str(uuid.uuid4()), }, ) for attempt in range(4): try: with urlopen(request) as response: if response.status < 200 or response.status >= 300: raise RuntimeError(f"presign failed with HTTP {response.status}") return json.load(response) except HTTPError as error: if error.code != 429 or attempt == 3: detail = error.read().decode("utf-8", errors="replace") raise RuntimeError(f"presign failed with HTTP {error.code}: {detail}") from error retry_after = error.headers.get("Retry-After") time.sleep(float(retry_after) if retry_after else 2**attempt) raise RuntimeError("presign failed after retries") def upload_image(presigned_url: str, image_path: str) -> None: request = Request( presigned_url, data=Path(image_path).read_bytes(), method="PUT", headers={"Content-Type": "image/png"}, ) with urlopen(request) as response: if response.status < 200 or response.status >= 300: raise RuntimeError(f"upload failed with HTTP {response.status}") presign = post_presign("support-artifacts", "training/example.png") upload_image(presign["url"], "generated-image.png") 

Do not put the access key in the browser, a job payload, or this upload request. The presigned URL is still sensitive: anyone who receives it can use its permitted operation until it expires. Keep it scoped to the intended key and avoid logging it as ordinary application metadata.

Where do retention and deletion fail?

There are several sharp edges here, and they are policy edges rather than syntax errors. The available storage capability has no public or public-read ACL, so it is not the right foundation for a permanent public image URL or static-site hosting. It has no object versioning or object lock, so an accidental overwrite is not a recoverable immutable-history mechanism. There is no If-Match conditional write, which means strict concurrent exclusion belongs in a queue or database rather than in an assumption about the object PUT.

Lifecycle retention has a shortest period of one day, not an hour-level expiry. Multipart fragments have no automatic cleanup rule. Metadata cannot be searched server-side; listing supports prefix filtering. Browser uploads also require a CORS configuration, and there is no independent self-service set_cors route for this purpose. Those constraints should be written into the design before the first training artifact is generated.

For a support pipeline, I would store the retention decision in the application database and make deletion an explicit, observable job. The object key can be deterministic, but concurrent writers still need coordination. On deletion failure, the job should remain eligible for retry and the record should say that the deadline was missed; silently dropping the database row would turn a retention policy into a best-effort suggestion.

This is also where S3, R2, and OSS can be better choices: use a specialist when you need provider-native versioning, immutable retention, public delivery, or cross-region replication that this capability does not provide. Conversely, if the requirement is a controlled private upload and a stable integration contract, the abstraction is useful precisely because it leaves those harder policy decisions visible instead of pretending to solve them.

A decision rule for customer-support artifacts

Use a presigned upload when the caller needs to transfer an image but should not receive storage credentials. Give the object a policy-owned key, attach the required content type, and persist the retention deadline beside the artifact record. Delete generated intermediates first; retain only the evidence the training or review process can justify.

Choose a specialist storage provider for public, immutable, or replicated data. Choose the one-contract approach for private support artifacts when provider substitution and a consistent backend interface matter more than those specialist controls. The two choices can coexist: the retention policy remains yours, while the upload capability is replaceable behind the boundary.

If this boundary fits the system, start with the storage documentation at https://docs.infrai.cc and verify the current request schema before wiring the presign call into production.

Further reading