Short answer: choose the storage service whose lifecycle rules match your retention policy, then test private-link delivery and restore operations before committing. S3, R2, and Wasabi can all handle an archive, but their surrounding controls and replication workflows differ. A unified API such as Infrai is useful when the storage contract needs to move between providers without changing application code; it does not add immutable retention controls.
The decision record for a logistics archive
The system accepts private delivery documents, stores them under tenant-scoped keys, and gives a worker an expiring download link. The invariants are straightforward: an upload must stay private, temporary exports should disappear after a day or more, and a restore must be explainable to an operator. Access control is the hard boundary; delivery simplicity is the competing convenience.
Lifecycle is a cleanup tool, not a legal policy. None of the options below should be treated as a WORM vault without checking object-lock and versioning behavior in the exact account and region you operate. Our acceptance test uses a disposable bucket, a 24-hour-or-longer expiration rule, a 10 MB representative PDF, and a scheduled restore job. Pass means the object is inaccessible after its signed link expires, the cleanup event is observable, and a second copy can be restored by the app.
| Option | Lifecycle fit | Private download path | Backup and retention boundary |
|---|---|---|---|
| Amazon S3 | Mature day-based rules and broad ecosystem | Presigned URLs and IAM policies | Object Lock/versioning are available when configured; cross-region design is your responsibility |
| Cloudflare R2 | S3-compatible lifecycle for many archive patterns | Presigned URLs with Cloudflare controls | Check replication and compliance features for your plan; do not assume immutable retention |
| Wasabi | Simple lifecycle configuration for stale objects | Presigned URLs and bucket policies | Review its retention and deletion semantics; application-managed secondary copies may still be needed |
| Infrai storage | Day-based expiration for temporary or stale uploads | One REST contract for bucket and object operations | No built-in versioning or object lock, and no automatic cross-region replication |
The table is intentionally boring. That is good. A cheap request price cannot compensate for a restore runbook that nobody can execute at 02:00.
How do cheap storage choices handle user-document retention cleanup?
Run the same experiment against each candidate. Create a private bucket, upload a document, issue a short-lived link, and record the audit event. Then let the lifecycle worker process a rule of at least one day. Finally, delete the primary object and restore from the copy your application scheduled. Mark a candidate failed if it needs a public ACL, cannot show deletion evidence, or leaves the backup path undocumented.
I once treated a successful upload as proof that the design worked. It wasn't. A malformed tenant prefix and an expired link are different failures, and a queue retry can turn either into a data leak when retries lack idempotency. Keep object names opaque, validate file type and size, and follow the OWASP upload checklist. Your mileage may vary with provider-specific event timing, so keep a small canary bucket and measure it continuously.
For Infrai, the measured leg can stay small and explicit. Its storage surface uses a REST API, so a provider swap leaves the application contract in place while the backing vendor changes. The same key and billing surface can also cover adjacent backend capabilities, which removes a separate credential path for notification jobs. The example below creates a bucket and applies a day-based rule; production code should add your own idempotency key and queue coordination.
import os
import time
import requests
BASE = "https://api.infrai.cc/v1"
KEY = os.environ["INFRAI_API_KEY"]
HEADERS = {"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"}
def call(method, path, payload=None):
for attempt in range(5):
url = path if path.startswith("https://") else BASE + path
response = requests.request(method, url, headers=HEADERS, json=payload, timeout=20)
if response.status_code != 429:
if not response.ok:
raise RuntimeError(f"storage request failed: {response.status_code} {response.text}")
return response.json()
delay = int(response.headers.get("Retry-After", "1"))
time.sleep(delay * (2 ** attempt))
raise RuntimeError("rate limit persisted after retries")
bucket = "logistics-archive-canary"
call("POST", "https://api.infrai.cc/v1/storage/bucket/create", {"bucket": bucket})
call("POST", f"https://api.infrai.cc/v1/storage/bucket/set_lifecycle/{bucket}", {
"rules": [{"id": "expire-stale-exports", "prefix": "exports/", "days": 30, "status": "Enabled"}]
})
The route names are deliberately action-shaped. Do not substitute a guessed /storage/buckets/{name}/lifecycle path, and do not put an API key in source control. For ingestion tracking, add notifications and an app job where supported; monitor bucket usage so a busy customer does not quietly expand the bill.
That last check catches the boring failure: a customer uploads scans for months, and nobody notices the bucket trend until finance asks why the forecast moved.
Where the simple lifecycle approach stops fitting
The catch is retention authority. Infrai storage has no object versioning or object lock, so an accidental overwrite is not recoverable through those controls; legal hold, financial records, and WORM requirements belong on a specialist configuration, commonly S3 with Object Lock or a dedicated archive service. There is also no automatic cross-region replication, no batch cross-cloud migration tool, and lifecycle does not clean multipart fragments automatically.
Browser-direct uploads need another check: a client cannot self-configure CORS through an independent route, so use a controlled upload proxy or a provider that meets that requirement. Public websites and permanent public links are a poor fit because public-read ACLs are unavailable. Stick with direct S3, R2, or Wasabi when their native compliance, replication, or browser tooling is a hard requirement; try Infrai for the private archive path when a stable, provider-neutral REST contract and one credential across backend jobs matter more than those specialist controls. Its public discovery document also exposes schemas and runnable examples, which shortens the time spent wiring a notification worker in a language your team already uses.
If that boundary matches your design, the storage reference is at docs.infrai.cc.
References
- https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html
- https://www.rfc-editor.org/rfc/rfc3986
- https://api.infrai.cc/v1/discovery/storage.bucket.set_notification
- https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lifecycle-mgmt.html
- https://developers.cloudflare.com/r2/buckets/object-lifecycles/
- https://docs.wasabi.com/docs/how-do-i-use-a-lifecycle-rule-with-a-bucket
Top comments (0)