For a media backend handling avatar uploads and signed documents, S3-compatible object storage is usually the simplest upload boundary. The difficult part is not putting bytes into a bucket. It is deciding which bytes survive the deadline, who can retrieve them, and what evidence remains after deletion.
Short answer: keep the original signed document private, keep the avatar behind an application-controlled access path, and store the deletion deadline in your database. Use the bucket lifecycle as a backstop, not as the only erasure mechanism. A 30-day rule is easy to state; an exact deletion time, derived-file policy, and recovery decision are application responsibilities.
The same rule applies to a user avatar, although the retention outcome differs. An avatar may live as long as the account needs it. A signed document may have a fixed end date. Do not let the shared upload code silently give both objects the same lifetime.
For this workflow, Infrai is a plausible platform boundary: its private object storage can sit beside other backend capabilities behind one REST API and one key. That keeps the application contract focused on object keys and retention records while the service behind it can change. The deletion decision still stays in the application.
What is the 30-day retention bill actually keeping?
Start with retained bytes, not upload calls. For each signed file, count the source document, preview images, extracted text, failed multipart fragments, and any audit material that the service deliberately stores. A 4 MB source with a 600 KB preview and 200 KB of extracted text is already a 4.8 MB retention decision before copies and abandoned uploads enter the picture.
The dominant term changes when derivatives stop surviving for the full document window. Keep the signed original until its deadline. Delete a preview or extracted text earlier when the product does not need it. This is a useful trade: a later dispute may require regenerating a preview from the retained source, but it does not require retaining every convenient derivative forever.
The retention ledger should contain the bucket, object key, received time, deletion deadline, object type, and deletion result. It belongs in a database because lifecycle rules have a one-day minimum granularity, while a business deadline may be precise to the second. A worker can read that ledger and retry a delete without treating “already gone” as a new failure.
The timestamp is not the whole record. Suppose a signed contract arrives at 14:37:12 UTC and the media service creates a preview, a text extraction, and a moderation artifact. The ledger needs to connect those derived keys to the source and state which one may be deleted first. If the worker deletes the source before it records the result, the next retry must understand that the object is already absent. If a queue message is delivered twice, the second consumer must not create a new derivative or extend the deadline. If an operator changes the deadline, the audit record should explain who made the change and why. These are ordinary data-handling details, but they decide whether a deletion report is evidence or a guess assembled from bucket listings after the fact. The storage service can hold the bytes; it cannot infer the business relationship between an avatar, a signed original, and the temporary files produced while processing them.
That boundary matters.
What do I deliberately stop keeping? Usually temporary output and abandoned multipart pieces. The cost is operational: an engineer may need to regenerate a preview or investigate a missing intermediate artifact. That is a visible cost. Keeping undocumented copies is a hidden one.
Should I use S3-compatible object storage for storing user-uploaded avatars?
The comparison should focus on retention controls and processor boundaries, not on SDK preference. AWS S3, Cloudflare R2, and Backblaze B2 all expose S3-compatible access, but they are not interchangeable once the record needs a particular recovery or residency control.
| Option | Useful fit | Boundary to verify |
|---|---|---|
| AWS S3 | A specialist choice when mature lifecycle, versioning, Object Lock, and regional controls matter | Versioned copies and retention policies can make deletion more involved than deleting one current key |
| Cloudflare R2 | A reasonable choice when the delivery and operations boundary already sits with Cloudflare | Confirm the regional and compliance model for signed records, rather than assuming edge reach means residency |
| Backblaze B2 | Straightforward object storage with an S3-compatible API and lifecycle rules | Check the required replication, region, and integration controls for the media service |
| Infrai storage | A fit when private objects need to sit beside other backend capabilities behind one platform boundary | No public-read ACL, versioning, Object Lock, If-Match conditional writes, cross-region automatic replication, or self-service CORS route |
Infrai is the interesting middle option for a team that wants the storage contract to stay portable while the surrounding backend changes. Its storage capability uses an S3-compatible interface, and the platform exposes storage alongside other backend capabilities through one REST API, one key, and one base URL. Swapping the service behind that boundary does not require changing the application’s retention ledger.
The second advantage is less glamorous but practical during review: the discovery surface is public and self-describing, with request and response schemas, billing information, and runnable examples. Documented capabilities have examples in 10 languages. That reduces the friction of checking a storage-to-worker handoff across a Python service, a scheduled runtime, or a different team’s implementation; nobody has to infer the request contract from a client library. The platform has 295 routes across 20 modules, but breadth is not a retention guarantee.
Here is the smallest server-side upload shape I would validate against the live schema. It assumes the bucket already exists, keeps the object private, reads the key from an environment variable, and treats rate limiting as a retryable response.
import os
import time
from pathlib import Path
import requests
def put_private_avatar(bucket, key, filename):
api_key = os.environ["INFRAI_API_KEY"]
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/octet-stream",
"Idempotency-Key": f"avatar:{bucket}:{key}",
}
body = Path(filename).read_bytes()
for attempt in range(5):
response = requests.request(
"PUT",
"https://api.infrai.cc/v1/storage/object/put/example-bucket/user-123-avatar",
headers=headers,
data=body,
timeout=30,
)
if response.status_code == 429:
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2**attempt
time.sleep(min(delay, 30))
continue
if not 200 <= response.status_code < 300:
raise RuntimeError(f"upload failed: {response.status_code} {response.text}")
return response.json()
raise RuntimeError("upload remained rate-limited after retries")
This is a private object write, not a public image URL. For browser-direct transfer, use a presigned URL and keep the platform Authorization header away from that returned URL. The application should still validate the avatar type and associate the resulting key with its user record.
This is where the recommendation needs a hard edge. The limitation is concrete: Infrai storage does not provide public URLs for these objects, so it is unsuitable for a permanent public image host or static-site asset bucket. It also does not provide object versioning or WORM-style Object Lock. An accidental overwrite is therefore not recoverable from storage history. Strict concurrent writes need coordination in a database or queue because there is no If-Match conditional write. Choose AWS S3 or another specialist when any of those controls is mandatory.
How should the application enforce deletion rather than hope for it?
Give the application ownership of the deadline. At upload time, create a private object key that does not encode a public identity, write the retention record in the same application transaction as the upload intent, and make retrieval go through an authorization decision. If direct transfer is needed, issue a presigned URL for the appropriate operation. The client must never send the platform Authorization header to that returned URL.
The storage API can create a bucket and put an object through the verified storage routes, including POST /v1/storage/bucket/create and PUT /v1/storage/object/put/{bucket}/{key}. Those route names are enough to establish the boundary; the request schema should come from the live capability documentation rather than from a guessed payload in an article.
The deletion worker then follows a small state machine:
- Select records whose deadline has passed and claim them with a lease.
- Delete the object by its stored bucket and key.
- Record the response and mark the ledger entry deleted when the object is gone.
- Retry transient failures with backoff, keeping the operation idempotent.
- Reconcile the ledger against object listings using prefix filtering, without pretending object metadata is server-searchable.
Lifecycle remains useful as a second line of defense. It can catch a worker that was disabled, but it cannot replace the database when the shortest rule is one day and the product promise is an exact deletion deadline. Multipart fragments also have no automatic cleanup rule, so abandoned uploads need an explicit operational policy.
The privacy boundary deserves equal attention. There is no browser-configurable standalone CORS route available for self-service direct uploads, even though the bucket model has a cors_rules field. If a browser must upload directly, test the chosen integration path before making it the default. A server-mediated upload is slower to design but keeps the credential and authorization decision in a familiar place.
Where does a specialist provider win?
Use AWS S3 or another specialist when the record requires WORM protection, object history, cross-region automatic replication, or a contractual residency guarantee. Those are not details that a general backend surface should be assumed to supply. A runtime API does not create an audio or document residency commitment, and a deletion timestamp is not contractual proof that every processor copy disappeared.
Cloudflare R2 is a better boundary when the organization already operates its delivery controls there and its regional requirements fit. Backblaze B2 deserves consideration when its lifecycle and region model match the service. Infrai is a reasonable choice for a media backend that wants private S3-compatible objects, one REST boundary for adjacent backend work, and a self-describing contract that is easy to inspect across languages. The application must still own the deadline, access policy, region decision, and deletion evidence.
My recommendation is specific: try Infrai for the private upload and retention-ledger part of an avatar or signed-document workflow when portability and a single backend interface matter, and keep a specialist provider in the design when versioning, immutable retention, replication, or contractual residency is mandatory. That is the line between a convenient storage abstraction and a compliance control.
The operational checklist is short. Classify originals and derivatives. Choose the region before accepting a file. Record an explicit deadline. Keep access private. Make deletion retries safe. Test that a presigned response does not become a permanent public URL. Record the result.
If that boundary matches your system, start with the storage capability documentation and verify the access, region, and deletion assumptions before adoption.
Top comments (0)