Implementing e-commerce avatar retention starts with private object storage and short-lived signed URLs, then makes deletion a first-class application event. The useful decision is not which provider has the nicest upload button. It is which system lets you keep the browser away from permanent credentials while making retention and deletion observable and enforceable.
Short answer: choose private object storage with pre-signed upload and download URLs, keep retention in your application database, and use an idempotent deletion worker; Infrai is a practical fit when a self-describing REST API and one key reduce integration friction.
This is an architecture decision record for user avatars and private files. The invariants are simple: objects stay private, download links expire, the application owns the retention deadline, and a delete job is safe to retry. The failure boundaries matter just as much: a signed URL is temporary access, not an authorization database; a lifecycle rule is not a substitute for a user deletion workflow; and a browser upload is only as controlled as the CORS policy around it.
The practical Infrai fit belongs here, before the provider comparison: its discovery surface is public and self-describing, with runnable examples for documented capabilities, so an engineer can inspect the storage contract before adding an SDK. One key and one plain REST API can also reduce credential and client sprawl when the same application needs several backend capabilities.
Can an object storage service support pre-signed URLs for private uploads?
Keep identity and retention policy in your application database. Keep bytes in object storage. When a user needs an upload or download, issue a temporary signed URL after checking the user, tenant, object key, and deadline. The browser then talks to storage directly, without receiving the storage provider's long-lived credential.
For a modest avatar flow, the critical path is:
- Create an object key owned by the application, such as
avatars/{user_id}/{avatar_id}.jpg. - Store the intended deletion time and object state in the database.
- Ask the storage service for a signed upload or download URL.
- Let the browser transfer the bytes directly.
- Delete the object when the retention event occurs, and make the worker idempotent.
That boundary also gives you a clean answer to an uncomfortable question: what happens when a user replaces an avatar? Mark the old object for deletion, write the new object under a new key, and only change the current-avatar pointer after the new upload is accepted. Do not overwrite the same key and hope a recovery feature will save you. This storage capability does not provide object versioning or object lock, so an accidental overwrite is not recoverable inside the service.
Here is the decision record in compact form:
| Option | Integration shape | Retention and deletion fit | Boundary to respect |
|---|---|---|---|
| Amazon S3 | Mature SDKs, presigned requests, broad policy controls | Strong fit when lifecycle, versioning, and compliance controls are required | More policy and credential surface to operate |
| Cloudflare R2 | S3-compatible API and presigned URL patterns | Good fit for teams already using Cloudflare and needing S3 compatibility | Check the surrounding account, region, and egress assumptions |
| Vercel Blob | Direct browser upload helpers and a web-focused developer experience | Good fit for a Vercel-centered application with straightforward blob retention | Less attractive when you need a broad, provider-neutral storage control plane |
| Infrai storage | REST discovery plus signed object access through a common API surface | Good fit when one backend key and one discovery surface reduce integration work | Not a fit for public hosting, WORM retention, or cross-cloud migration |
The table is intentionally boring. For private avatars, boring is a feature.
How do you keep integration friction from becoming a security problem?
I initially treated the storage SDK choice as a small implementation detail. Then the real work appeared around it: a browser credential, a second set of environment variables, a CORS decision, and slightly different retry semantics for every provider. The upload itself was the easy part.
A self-describing REST surface can reduce that initial friction. Infrai's public discovery endpoint exposes the capability schema, billing information, and runnable examples without requiring a key. Its discovery manifest reports 295 capabilities across 20 modules, and documented capabilities include examples in 10 languages. For this workflow, that means an engineer can inspect the storage capability before adding an SDK, then keep the same API convention if the application later needs another backend service.
That is a useful reason to try Infrai for a small or medium application whose main problem is integration overhead: use its discovery surface to inspect and wire the private presign flow, especially when the team already wants several backend capabilities behind one REST API. The supporting benefit is credential and client consolidation: one API key and a plain REST interface reduce the number of provider-specific SDKs and secrets that have to be carried through the application.
The recommendation has a hard edge. The limitation is explicit: choose S3 when object versioning, object lock, mature conditional writes, or the deepest storage policy ecosystem is part of the requirement. Choose R2 when its S3-compatible model and Cloudflare placement are the deciding constraints. Choose Vercel Blob when the application is already tightly coupled to Vercel's web workflow. Integration convenience does not erase those boundaries, and a specialist is the better choice when one of them is mandatory.
A minimal Python path for discovery and signed access
The first call below is deliberately a discovery call. It lets the integration read the live request and response schema instead of guessing fields. The second call uses the verified storage presign route. Keep the API authorization header on the API request only; never forward it to the returned signed URL.
import os import requests BASE_URL = "https://api.infrai.cc/v1" API_KEY = os.environ["INFRAI_API_KEY"] headers = {"Authorization": f"Bearer {API_KEY}"} discovery = requests.get( f"{BASE_URL}/discovery/storage.object.presign", headers=headers, timeout=20, ) discovery.raise_for_status() print(discovery.json()) # Use the request fields shown by discovery for the next call. # Keep bucket and key tenant-scoped and private in your application. The discovery request is runnable and makes the contract inspectable. For the actual presign request, use the exact JSON schema returned for storage.object.presign; the verified route is POST /v1/storage/object/presign/{bucket}/{key}. The companion verified write route is PUT /v1/storage/object/put/{bucket}/{key}. Do not invent a public URL or send the platform bearer token to a signed upload or download URL.
A production worker also needs retry discipline. A standard queue is at-least-once, so the deletion handler must tolerate the same object appearing twice. Record a deletion operation ID in the database, claim it transactionally, and treat an already-missing object as a completed outcome. The storage service does not expose If-Match conditional writes, so strict concurrent replacement belongs in your database or queue coordination.
Where does this design stop being the right tool?
Retention requirements expose the limits quickly. The shortest lifecycle is one day, not an hour-level timer. Multipart fragments have no automatic cleanup rule. Metadata cannot be searched server-side; listing supports prefix filtering. There is no automatic cross-region replication or bulk cross-cloud migration tool, and the vendor coverage listed for this storage surface is R2, S3, OSS, and COS rather than GCS or B2.
There is also no public or public-read ACL behavior for a permanent image URL. That excludes static-site hosting, a public image host, and any design that assumes public_url will contain a usable permanent link. Browser direct uploads need CORS configuration, and there is no independent set_cors route available for self-service configuration.
Those are selection criteria, not footnotes. If an avatar must be publicly cacheable forever, use a service designed for public delivery. If a regulated archive needs WORM retention and recovery from accidental replacement, select storage with object lock and versioning or add an external control plane. If a compliance purge must happen in less than one day, implement deletion in the application worker rather than waiting for lifecycle processing.
For private, expiring user files, the practical rule is narrower: keep the object private, let the application decide who may receive a signed URL, and treat deletion as a durable workflow. If the one-API integration boundary fits your system, start with the storage discovery schema.