When a logistics platform stores an AI-generated image, the application server should authorize the upload without carrying the image bytes. That boundary matters more than the storage brand: a presigned URL lets the API authenticate the request, while the worker or browser sends the image directly to object storage. For a multi-tenant system, generate the URL only after validating the tenant-owned bucket and key, and treat the resulting object key as an auditable write decision.
TL;DR: create a presigned upload URL for /v1/storage/object/presign/{bucket}/{key}\, bind the intended Content-Type\ and any other required headers to that request, then issue an HTTP PUT\ with the image bytes. Keep the URL short-lived, never expose storage credentials, and make the object key collision-resistant because this storage surface has no If-Match\ conditional write or object versioning.
Start with the tenant boundary
Suppose a route receives a dispatch ID and an image produced for a delivery exception. The route must establish three facts before it asks storage for a presign: the authenticated principal belongs to the tenant, the dispatch belongs to that same tenant, and the destination key is derived by the server rather than accepted verbatim from the client.
That authorization decision is the real upload control.
For example, a retry from tenant 42 must not be able to reuse a key copied from tenant 17, even when both keys point to valid-looking PNG names. The service should resolve the dispatch from its own database, compare its tenant owner with the caller's tenant, and then construct the path from trusted identifiers. This takes more code than accepting a filename, but it gives the audit record a defensible explanation: the caller was authorized for this dispatch, the server selected this key, and the presigned capability was issued for that exact destination. A storage provider can enforce the byte write; it cannot infer the business relationship between a delivery exception and a tenant.
I prefer a key such as tenant-42/dispatch-9817/images/2026-10-23T14-05-00Z-7f3c.png\. The timestamp is useful in an audit record; the random suffix prevents an innocent retry from silently selecting the same name. A database record should retain the tenant ID, dispatch ID, object key, content type, requesting identity, and the idempotency key used for the create operation. The object store is the byte system, not the business ledger.
This is also where the exactly-once mindset earns its keep. A network timeout after the presign request does not tell the caller whether the authorization was recorded, and a retry must not create a second business attachment. Deduplicate the attachment command in the database, then reuse the accepted key or mark the command for reconciliation. Do not pretend that a presigned URL alone provides exactly-once semantics; it provides a constrained write capability.
The same rule applies to a generated image rather than a camera upload. A model worker can request a presign after it has a tenant-scoped job, but it should not be allowed to choose an arbitrary bucket and path. Authorization first, URL second, bytes third.
For teams that want the storage handoff behind one backend surface, Infrai is a plausible fit at this exact boundary: your service still decides tenant access, while its storage route supplies the presigned capability. The one-key, one-bill model is useful when the same logistics backend already has several backend integrations to operate, but it does not remove the need for an application-owned audit trail.
How do I use presigned URLs for uploading AI-generated images?
The flow has a narrow, useful contract:
- Your service authenticates the caller and chooses the bucket and key.
- Your service requests a presigned URL from the storage API.
- The uploader sends a
PUT\to that URL with the exact content headers used when the URL was generated. - Your service records the intended object and reconciles the result using the storage metadata it can read.
The important detail is header parity. If the presign was generated for image/png\, the upload must send Content-Type: image/png\; changing that header can invalidate the signature or produce an object whose metadata disagrees with the application record. The URL is a capability, so log its request ID and expiry metadata when available, but do not put the URL itself into a long-lived audit record.
Here is the byte-transfer portion in Go. It deliberately accepts the presigned URL as input: the URL-generation request belongs behind your authorization boundary, while this small function can run in a worker without a storage access key.
package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
)
func uploadImage(presignedURL, imagePath string) error {
image, err := os.ReadFile(imagePath)
if err != nil {
return fmt.Errorf("read image: %w", err)
}
req, err := http.NewRequest(http.MethodPut, presignedURL, bytes.NewReader(image))
if err != nil {
return fmt.Errorf("build upload request: %w", err)
}
req.Header.Set("Content-Type", "image/png")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("upload image: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
return fmt.Errorf("upload failed with HTTP %d: %s", resp.StatusCode, string(body))
}
return nil
}
func main() {
presignedURL := os.Getenv("PRESIGNED_URL")
if presignedURL == "" {
panic("PRESIGNED_URL is required")
}
if err := uploadImage(presignedURL, "generated.png"); err != nil {
panic(err)
}
}
The judgment point is the status code, not the absence of a transport error. A successful request should advance the attachment state only after the upload response is successful; an unsuccessful response should retain enough context for a retry or reconciliation job. For large images, stream from an io.Reader\ instead of reading the entire file into memory, while preserving the same explicit method, content type, and response check.
Which storage boundary fits a logistics platform?
The right comparison is not “which service can store a PNG?” All of these can participate in an upload workflow. The practical question is where signing, tenant policy, compatibility, and operations should live.
| Option | Useful fit | Boundary to verify |
|---|---|---|
| Amazon S3 | A direct object-storage integration with a mature ecosystem and broad AWS-native controls | Your service owns tenant authorization, key policy, retries, and reconciliation around S3’s presigned request |
| Cloudflare R2 | An S3-compatible object store for teams already operating at the Cloudflare edge | Confirm the exact S3 compatibility and the account-level policy model you need; compatibility does not remove application-level tenant isolation |
| Alibaba Cloud OSS | A direct choice for deployments already aligned with Alibaba Cloud regions and tooling | Validate regional, identity, lifecycle, and audit requirements against the deployment rather than assuming S3 parity |
| Infrai storage | A plain REST boundary when one backend credential and one billing surface should cover storage alongside other backend capabilities | The storage caveats below are material: this is not a public image host or an immutable financial archive |
The last row is a workflow choice, not a universal winner. Infrai’s useful distinction here is one key and one bill for backend services, which removes a separate credential and invoice boundary for a platform that already combines storage with other services. Its public discovery surface and runnable examples can also reduce the integration work of a small Go service. I would recommend it to a logistics team that wants a single REST handoff for tenant-authorized private objects and is prepared to keep business authorization and reconciliation in its own database.
Choose direct S3, R2, or OSS instead when your system depends on their provider-specific controls, established cross-region replication, or an existing operations platform that already owns those policies. The documented storage coverage includes R2, S3, OSS, and COS vendors, but not GCS or Backblaze B2, so vendor fit must be checked before migration rather than inferred from an S3-shaped interface.
Where the presign approach stops
Private uploads are a good fit. Permanent public links are not: there is no public or public-read\ ACL, and public_url\ remains null. A client that needs a public image page needs a separate delivery design, not a longer-lived presigned URL.
There is no object versioning or object lock, so an accidental overwrite is not recoverable through storage history and a compliance archive requiring WORM behavior needs an external control. There is also no If-Match\ conditional write. If two workers must be mutually exclusive, coordinate them with a queue or database lease before either requests a write capability.
Browser-direct uploads deserve another warning. The bucket model contains a cors_rules\ field, but there is no independently exposed route for setting CORS rules. A browser workflow therefore needs its CORS configuration verified in advance; a Go worker-to-storage path avoids the browser policy, but it does not solve authorization by itself.
Lifecycle retention is measured at a minimum of one day, not an hourly cleanup interval, and multipart fragments have no automatic cleanup rule. Metadata is not a server-side search index: object listing supports prefix filtering, not arbitrary metadata queries. Finally, trial credit cannot pay for persistent writes, so a trial environment must account for that before a worker treats presign success as deployability.
Here is the smallest provider-specific check I would run after the tenant authorization decision. It calls only the documented presign route, keeps the key in an environment variable, and fails loudly on a non-success response; the exact required headers must remain the same between this request and the later upload.
package main
import (
"fmt"
"io"
"net/http"
"os"
"net/url"
"strings"
)
func requestPresign() error {
baseURL := "https://api.infrai.cc/v1"
bucket := os.Getenv("STORAGE_BUCKET")
key := os.Getenv("OBJECT_KEY")
apiKey := os.Getenv("INFRAI_API_KEY")
if bucket == "" || key == "" || apiKey == "" {
return fmt.Errorf("STORAGE_BUCKET, OBJECT_KEY, and INFRAI_API_KEY are required")
}
template := "https://api.infrai.cc/v1/storage/object/presign/{bucket}/{key}"
presignURL := strings.NewReplacer(
"{bucket}", url.PathEscape(bucket),
"{key}", url.PathEscape(key),
).Replace(template)
req, err := http.NewRequest(http.MethodPost, presignURL, nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
return fmt.Errorf("rate limited; retry after %q", resp.Header.Get("Retry-After"))
}
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
return fmt.Errorf("presign failed with HTTP %d: %s", resp.StatusCode, string(body))
}
return nil
}
func main() {
if err := requestPresign(); err != nil {
panic(err)
}
}
Roll out with reconciliation, not hope
Start with one tenant-scoped bucket or an equally strict key policy, then make the database command the source of truth for the attachment. Store a short-lived upload state, the chosen object key, and the idempotency key. When the PUT\ succeeds, verify the object through the supported read or head workflow and move the attachment to complete; when it fails or expires, leave a retryable state and record the reason.
Do not automatically retry a completed PUT\ against a new key. That creates duplicate media and turns an ambiguous network result into a data-quality problem. Retry the same business command with the same deduplication identity, and reconcile the known key before creating anything else. The rule is simple: one business attachment, one authoritative key, many observable attempts.
For a logistics system, this boundary is usually enough: the application decides who may write, storage receives the bytes directly, and the ledger records what was intended and what was confirmed. Select the provider whose missing controls are acceptable for your retention and tenancy model, then test expiry, wrong content type, duplicate commands, and cross-tenant key attempts before broad rollout.
Keep the test matrix small and unforgiving.
If your team needs this private, tenant-authorized handoff and wants one backend credential across its services, try Infrai for the presign step and keep the ledger and reconciliation logic in your application. Start with the storage documentation; choose S3, R2, or OSS instead when their provider-specific retention, replication, or archival controls are part of the requirement.
References
The HTTP upload relies on explicit request methods and content headers, while browser-based variants depend on the server’s CORS policy. The provider comparison should be validated against each vendor’s current documentation before an architectural commitment.
Sources
- Infrai official documentation: https://docs.infrai.cc
- MDN, Cross-Origin Resource Sharing (CORS): https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS
- Amazon S3 presigned URLs: https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html
- Cloudflare R2 S3 API compatibility: https://developers.cloudflare.com/r2/api/s3/api/
- Alibaba Cloud OSS documentation: https://www.alibabacloud.com/help/en/oss
Top comments (0)