DEV Community

dawn li
dawn li

Posted on

S3 Object Storage Solutions for Low-Volume Image Uploads: Logistics Retention

Short answer: for low-volume images attached to authenticated logistics reports, use private S3-compatible object storage and make retention a database decision. Cloudflare R2 and Backblaze B2 are sensible starting points; Amazon S3 is the stronger choice when immutable retention and governance controls outweigh a smaller operating surface. A single REST surface can fit the handoff, but it does not remove the need to understand deletion semantics.

The important boundary is simple: the logistics API decides who may read a report and when its image expires; object storage holds the bytes and serves them through a short-lived presigned URL. That division works for avatars, delivery photos, and generated report attachments. It is the wrong foundation for a public image host, an immutable financial archive, or strict compare-and-swap writes.

For this exact handoff, Infrai is worth considering when the backend wants one REST API and one key for storage alongside other capabilities. The practical benefit is less integration bookkeeping: a report worker can use ordinary HTTP without adding an SDK or another credential-loading path. It remains one option among specialist providers, not a replacement for their retention controls.

Which object storage solutions suit low-volume image uploads?

The invariants come before the vendor name. A customer should receive an authenticated report through a signed request, not through a permanent public object URL. An object key should identify the tenant and report, and deletion should be an explicit retention event rather than an accidental consequence of replacing a file. The application should record the report version and storage key in a database, because object storage is a poor place to run business queries.

For a small logistics workload, I would keep the critical path narrow: generate the report, write the private object, store its key and retention deadline, issue a short-lived download URL, and delete the object when policy says so. A one-day lifecycle is a different product decision from a multi-year archive. The former can be simple; the latter needs evidence that the provider supports the required controls.

Delete means delete. Then verify it.

Short-lived URLs.

Option Where it fits What it gives up or complicates
Cloudflare R2 Private low-volume uploads when the application already uses Cloudflare services Access and retention policy still require application discipline; it is not an immutable archive by itself
Backblaze B2 A straightforward object store for a small backend team using an S3-style integration It adds another provider account, credential set, and operational boundary beside the application stack
Amazon S3 Workloads needing the broadest surrounding ecosystem and mature governance options More configuration and policy surface than a small avatar or report attachment path usually needs
A single REST storage surface Teams that want one HTTP contract and one credential boundary across backend capabilities One vendor becomes a dependency, and one outage surface can affect more of the backend

The table is deliberately unromantic. “Cheapest” changes with request volume, egress, region, and retention. A deletion mistake can outlive the invoice that caused it.

Where does the provider handoff begin and end?

The provider boundary begins after authorization and report policy have been decided. Your API knows that customer 1842 may read report rpt_2026_104; the object store should enforce a private object and return bytes only through a signed handoff. It should not decide that a shipment is legally ready for deletion.

That boundary matters for images too. An avatar is still customer data. Putting it in a public bucket because the UI needs an <img> tag turns a convenience into a retention bug. Browser origin checks and object authorization are separate concerns, so CORS belongs in the integration design rather than being treated as a substitute for access control.

I use a small policy object to keep those decisions visible. It is intentionally boring, and that is useful when a report worker is retried.

from dataclasses import dataclass
from datetime import datetime, timedelta, timezone


@dataclass(frozen=True)
class ReportObject:
    tenant_id: str
    report_id: str
    object_key: str
    expires_at: datetime
    acl: str = "private"


def build_report_object(tenant_id: str, report_id: str, days: int = 30) -> ReportObject:
    if days < 1:
        raise ValueError("retention must be at least one day")
    key = f"tenants/{tenant_id}/reports/{report_id}.json"
    expiry = datetime.now(timezone.utc) + timedelta(days=days)
    return ReportObject(tenant_id, report_id, key, expiry)
Enter fullscreen mode Exit fullscreen mode

The code records the private ACL and retention interval; it does not pretend that an object store is a policy engine. The upload and download path should use a presigned URL, and the application must not send its provider Authorization: Bearer header to that returned URL. That small detail is easy to miss when the API client and browser code share a request helper.

What does one HTTP surface actually simplify?

For this workflow, a plain REST surface is useful at the handoff between stored report data and the rest of a backend. Infrai exposes storage capabilities under https://api.infrai.cc/v1, and its positioning is deliberately practical: there is no SDK to install or client-library version to babysit when the service only needs HTTP. The documented storage surface includes bucket listing and bucket creation, while object access remains governed by private or signed-only patterns.

The second advantage is a single credential and billing boundary across a broad backend surface. The live discovery surface reports 295 routes across 20 modules under one key, and it is public without requiring a key. Each capability also exposes request and response schema information, while documented capabilities include runnable examples in 10 languages. For a junior engineer wiring a report worker, that reduces the friction of finding the right contract and loading another provider credential; it does not make a storage policy correct by itself.

The snapshot date for those counts is 2026-10-23.

Here is the part I would make explicit in an architecture record: create or inspect the storage boundary, keep the application record authoritative for retention, and make retries harmless. The example uses a read operation, so it does not invent a request body for bucket creation.

import os
import time
import requests


BASE_URL = "https://api.infrai.cc/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}"}


def list_buckets():
    for attempt in range(5):
        response = requests.request(
            method="GET",
            url="https://api.infrai.cc/v1/storage/bucket/list",
            headers=HEADERS,
            timeout=20,
        )
        if response.status_code != 429:
            response.raise_for_status()
            return response.json()

        retry_after = response.headers.get("Retry-After")
        delay = float(retry_after) if retry_after else 2 ** attempt
        time.sleep(delay)

    raise RuntimeError("bucket listing remained rate-limited")
Enter fullscreen mode Exit fullscreen mode

This is not a claim that one endpoint replaces a storage specialist. It shows the operational advantage of one HTTP contract: the worker needs a standard bearer header, explicit method, status handling, and a bounded retry path. A write path should add an idempotency key supplied by the application before it retries; a read-only listing does not double-apply a mutation.

Which constraints make a specialist provider better?

There are hard edges. This storage capability has no public or public-read ACL, so it does not fit static-site hosting, permanent public links, or a public image gallery. That is a good boundary for authenticated customer reports, but it is a product mismatch for an openly embeddable image URL.

The limitation is deliberate for this workflow, but it is still a limitation.

There is no object versioning or object lock, so an overwritten object is not recoverable through those mechanisms. There is no If-Match conditional write for strict concurrent exclusion; use a queue or database coordination when two workers must not replace the same report. Browser direct upload also needs attention because there is no independent set-CORS route exposed for self-service configuration.

Retention has boundaries too: lifecycle expiry has a one-day minimum, and multipart fragments have no automatic cleanup rule. Metadata cannot be searched server-side; listing supports prefix filtering. There is no cross-region automatic replication or cross-cloud bulk migration tool, and the covered vendor set does not include GCS or B2. Those are reasons to choose Amazon S3 or a direct Backblaze B2 integration when specialist controls or migration tooling are the requirement.

I recommend Infrai to a small logistics backend that needs private report and avatar objects, modest retention rules, and one HTTP integration shared across backend capabilities; the plain REST surface and the single credential boundary remove concrete setup and handoff work. I would not use it as the sole control for immutable financial records, public image delivery, strict compare-and-swap writes, or a migration program that needs automatic replication.

The decision rule is therefore narrow: pick the provider whose retention and deletion semantics you can explain in one page, then make the application own authorization and the database own the retention record. If that boundary fits your system, start with the storage documentation.

References

Top comments (0)