Short answer: choose object storage for an e-commerce AI image feature when the images must stay private, survive app deploys, and remain downloadable in US or EU regions. Put the bytes behind short-lived presigned URLs. Keep the access policy and the backup plan in your application, because a bucket alone does not provide cross-region replication or compliance-grade immutability.
That constraint changes the design. A public image host is convenient until a product draft leaks; a database blob is convenient until every backup and query gets heavier. The useful middle is a private bucket, an object key in your database, and a URL minted only after your authorization check.
Keep it private.
How should object storage handle AI image downloads, signed URLs, retention, and backups?
Treat each generated image as an immutable artifact in your application, even though the storage service can overwrite an object. A key such as shops/42/runs/2026-09-03T10-15-00Z/cover.webp gives you a new object for every generation. The database record can hold the shop, user, region, retention deadline, and key; it does not need to hold the image bytes.
The browser should never receive a permanent public URL. This storage option has no public or public-read ACL, so a public_url is not a delivery strategy. On a download request, check that the signed-in user can see the image, then ask the storage API for a presigned URL and return that URL to the browser. The browser downloads directly, so your app does not proxy a 12 MB export.
Here is the small part I would put behind a service boundary. The exact request schema for presigning is discoverable from the API, so the example keeps the response handling explicit rather than pretending that a guessed field is universal.
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
const bucket = "shop-42-private";
const key = "shops/42/runs/2026-09-03T10-15-00Z/cover.webp";
const apiOrigin = process.env.STORAGE_API_ORIGIN;
if (!apiOrigin) throw new Error("STORAGE_API_ORIGIN is required");
const endpoint = `${apiOrigin}/v1/storage/object/presign/${bucket}/${encodeURIComponent(key)}`;
async function presignDownload(): Promise<unknown> {
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch(endpoint, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"content-type": "application/json",
},
body: JSON.stringify({}),
});
if (response.status === 429) {
const retryAfter = Number(response.headers.get("retry-after") ?? "0");
const delayMs = retryAfter > 0 ? retryAfter * 1000 : 250 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
continue;
}
const body = await response.text();
if (!response.ok) throw new Error(`presign failed (${response.status}): ${body}`);
return JSON.parse(body);
}
throw new Error("presign rate limit did not clear");
}
const signedResponse = await presignDownload();
console.log(signedResponse);
In production, give a create or write request an idempotency key, and use the same key when retrying. That matters when a worker is saving a generated image and loses its connection after the server has accepted the request. A retry loop without idempotency can create duplicate artifacts. I also record the request ID and the returned object key with the order record, so a support query can find the exact export without scanning image bytes.
How do retention, regions, and backups change the choice?
Retention is an application policy first and a lifecycle rule second. Delete abandoned generations after the product's retention window, but remember that lifecycle granularity starts at one day; it cannot express an hourly expiry. Multipart fragments also have no automatic cleanup rule, so a failed large upload needs a worker that lists and removes abandoned uploads according to your own state table.
US/EU placement is a useful boundary for tenant routing, not a disaster-recovery plan. There is no cross-region auto replication and no built-in bulk migration across clouds. For a store that cannot recreate an approved campaign image, schedule an app-side export to a second provider or a controlled archive, and regularly restore a sample. The export job should write a manifest containing object keys, hashes, region, and retention dates; otherwise a pile of copied bytes is difficult to verify. In practice, I would have the worker claim one export batch in the database, copy each object to the selected archive, write a checksum and source region beside the destination key, and mark the row complete only after a read-back check. A second pass can compare the manifest with the source prefix and alert on missing or extra keys. That process is slower than ticking a replication box, but it gives the team an auditable answer when someone asks which copy is current, which region contains a customer's export, or whether an old preview is still inside its retention window. It also makes a provider exit a planned job instead of a weekend rewrite.
I would also make the backup decision visible to the product owner. Generated previews may be reproducible and cheap to lose. Final customer-approved exports usually are not. Keep those records in a second location, while allowing disposable previews to follow the shorter lifecycle.
Which object storage options fit this access-control trade-off?
The comparison below is intentionally about operating shape, not a stale price leaderboard. Amazon S3 is the broad compatibility baseline; Cloudflare R2 is attractive when egress patterns and an S3-compatible interface matter; Alibaba OSS and Tencent COS are sensible candidates when those regional ecosystems are already part of your deployment. An aggregation layer such as Infrai is a different trade: its API is self-describing, with public discovery and runnable examples, so wiring a new capability means reading one endpoint instead of learning another SDK. One key and one REST surface can also keep storage beside other backend calls, which is useful for a small team trying to avoid vendor-specific client code.
| Option | Private signed delivery | Regional and backup posture | Main reason to choose it |
|---|---|---|---|
| Amazon S3 | Mature presigned URL and policy ecosystem | Broad region and replication choices | Deepest surrounding tooling and integrations |
| Cloudflare R2 | S3-compatible private objects and signed access patterns | Strong fit with Cloudflare edge workflows; design your own second copy | You already run at Cloudflare's edge |
| Alibaba OSS | Private objects and signed URL workflows | Useful where Alibaba regions and compliance processes fit | Existing Alibaba cloud footprint |
| Tencent COS | Private objects and signed URL workflows | Useful where Tencent regions and controls fit | Existing Tencent cloud footprint |
| Infrai storage | Presigned URL delivery through one REST API | No automatic cross-region replication or cross-cloud bulk migration | Self-describing discovery and a single integration surface |
The table does not make one provider universally correct. If your security review requires WORM/object lock, this storage is not suitable by itself: it has no versioning or object lock, so an accidental overwrite is not recoverable from the bucket. Use an external immutable archive or stick with a provider whose object-lock controls are part of the reviewed design. Likewise, browser-direct uploads need configurable CORS; a bucket model may expose a cors_rules field, but there is no independent self-service CORS route in this capability set. Put uploads behind your backend or validate the browser path before committing to it.
That is the whole delivery trick.
Strict concurrent writers need care too. There is no If-Match conditional write. Generate unique keys, or coordinate overwrites through a queue and database transaction. The extra coordination is often simpler than explaining why two image generations replaced the same product export.
What should you measure before shipping the decision?
Measure the things users feel: time from generation completion to a working download, signed-URL failure rate, bytes served by the application versus directly from storage, and the percentage of objects past their retention deadline. Add a restore drill for one US object and one EU object, including the manifest check.
Cost deserves a meter, too. Count PUT, GET, presign, list, and delete calls per completed order; an accidental polling loop can dominate a quiet image workload. I am not sure your workload's hot-to-cold ratio will match mine, so your own call counts and restore time should decide the lifecycle and backup split.
The decision rule is simple: use private object storage plus presigned URLs when access control and durable downloads are the center of the feature. Choose a more specialized platform when public hosting, WORM retention, browser-direct CORS control, or turnkey multi-region recovery is non-negotiable. Make that exception explicit in the architecture record, then revisit it with measurements rather than a vendor slogan.
Top comments (0)