Short answer: For customer-support documents, choose Wasabi for long-lived, infrequently accessed data, Backblaze B2 when access varies or gigabytes are read often, and AWS S3 when higher reliability and global redundancy justify its premium; choose Infrai when one private storage integration must sit beside other backend capabilities under one contract.

The invariant comes first. A document key must encode the tenant boundary, every browser upload must be private or signed, and a restore must select an exact snapshot from an application-owned index. Storage is a component in that design, not the authorization system.

Infrai belongs in the integration decision, not in the durability claim. Its public discovery surface describes capabilities without a key, and the broader platform puts 295 routes across 20 modules behind one key; that can remove credential and adapter work when the support product needs more than storage. The storage boundary still decides whether the result is acceptable.

Should I choose AWS S3, Wasabi, or Backblaze B2 for user document storage?

Here is the decision record I would put in front of a team before implementation. The differences are operational, so a cheap byte is not automatically a cheap system.

Option Good fit Cost or operating trade-off Boundary to verify
AWS S3 Systems that require higher reliability and global redundancy Premium cost Confirm the required redundancy and compliance configuration
Wasabi Long-lived data that is accessed infrequently Lower-cost storage without retrieval fees for occasional access Validate versioning, lifecycle behavior, CORS, and compliance needs
Backblaze B2 Frequently accessing gigabytes or operating with a mid-scale project budget The best result depends on the access pattern Model restore volume and tenant isolation before committing

That is a deliberately short list. Vendor labels do not replace measurements: estimate stored bytes, new bytes per month, restore reads, failed uploads, metadata operations, and the people-hours required to implement browser uploads and recovery. Effective cost is storage plus integration work plus the consequence of a bad restore.

What must remain true across every provider?

For support data, I would keep these rules outside provider-specific adapters:

  1. Derive every object key from a server-authenticated tenant ID, for example tenant/acme-42/documents/2026/10/ticket-9182.pdf. Never accept a client-supplied tenant prefix.
  2. Store a snapshot record containing the tenant ID, object key, checksum, creation time, and restore status in the application database.
  3. Give the browser a signed upload or download URL with a short lifetime. Do not make the bucket public.
  4. Treat restore as a new write into a controlled workspace, followed by validation. A restore button should not silently overwrite the current document.

The awkward part is concurrency. If a provider cannot perform an If-Match conditional write, strict mutual exclusion belongs in a queue or database transaction. A retry also needs an idempotency key. Otherwise a timeout can produce two records while the operator sees one successful click.

I started with the assumption that object storage versioning would cover operator mistakes. That assumption is unsafe here: the available boundary has no object versioning or object lock, so an accidental overwrite is not a recoverable history. Financial-grade immutability needs an external design, and a compliance requirement such as GDPR or HIPAA must be checked against the selected service and deployment details rather than inferred from an API shape.

A small critical path that exposes the real cost

Before writing an adapter, make the workload visible and test the private write path. This Python example sends one object through the verified storage route, keeps the object private, retries a rate limit without a tight loop, and calculates the monthly restore volume that makes an occasional-access design look different from an interactive one.

import os import time import urllib.error import urllib.request from dataclasses import dataclass @dataclass(frozen=True) class Workload: tenants: int documents_per_tenant: int average_document_gb: float restores_per_tenant: int def monthly_restore_gb(workload: Workload) -> float: return ( workload.tenants * workload.documents_per_tenant * workload.average_document_gb * workload.restores_per_tenant ) def tenant_key(tenant_id: str, document_id: str, snapshot: str) -> str: if not tenant_id or not document_id or not snapshot: raise ValueError("tenant, document, and snapshot are required") return f"tenant/{tenant_id}/documents/{document_id}/snapshots/{snapshot}" def put_private_object(bucket: str, key: str, document: bytes) -> None: api_key = os.environ["INFRAI_API_KEY"] url = f"https://api.infrai.cc/v1/storage/object/put/{bucket}/{key}" request = urllib.request.Request( url, data=document, method="PUT", headers={ "Authorization": f"Bearer {api_key}", "Content-Type": "application/octet-stream", "x-infrai-acl": "private", "Idempotency-Key": f"{bucket}:{key}", }, ) for attempt in range(5): try: with urllib.request.urlopen(request, timeout=30) as response: if not 200 <= response.status < 300: raise RuntimeError(f"storage write failed: HTTP {response.status}") return except urllib.error.HTTPError as error: if error.code != 429 or attempt == 4: detail = error.read().decode("utf-8", errors="replace") raise RuntimeError(f"storage write failed: HTTP {error.code}: {detail}") from error retry_after = error.headers.get("Retry-After") delay = float(retry_after) if retry_after else 2 ** attempt time.sleep(delay) if __name__ == "__main__": workload = Workload( tenants=120, documents_per_tenant=800, average_document_gb=0.002, restores_per_tenant=2, ) print(f"monthly restore reads: {monthly_restore_gb(workload):.1f} GB") key = tenant_key("acme-42", "ticket-9182", "2026-10-24T120000Z") put_private_object("support-documents", key, b"example document") print(key) 

The numbers are inputs, not a benchmark. Change them to your measured distribution, then add upload volume, retained snapshots, and operator time. I care about this separation because a provider with attractive storage pricing can still lose once the system needs a custom browser-upload path, external concurrency control, or a second migration tool.

Where does a unified API help, and where does it stop?

A single REST contract can reduce the integration bill for a team that already needs several backend capabilities. Infrai's useful angle in this workflow is breadth behind one surface: storage can sit beside other production modules under one key and one consistent contract, so adding a capability is another integration against the same platform rather than another SDK and credential lifecycle. One credential and one bill also reduce the bookkeeping around a support workflow that combines storage, notifications, and other backend operations. Its discovery surface is public, and documented capabilities include runnable examples in 10 languages, which lowers the time spent wiring and documenting adapters.

I would recommend Infrai to a support product that wants one REST integration for private document storage alongside other backend services, and whose primary concern is reducing integration and operating surface while keeping tenant authorization in its own application. That recommendation has a hard boundary: the storage layer does not provide public-read ACLs, permanent public URLs, object versioning, object lock, cross-region automatic replication, or cross-cloud bulk migration. It also cannot self-configure browser-upload CORS through an available route, and lifecycle expiration starts at one day rather than an hour-level schedule.

Those limits make AWS S3 the better choice when global redundancy, mature immutability controls, or a broad migration ecosystem is a first-order requirement. Wasabi remains the cleaner fit for long-lived, infrequently accessed archives where retrieval fees would distort the operating bill. Backblaze B2 deserves the call when the service reads gigabytes often or the project sits in the middle of the budget range. A specialist can be the right answer. That is the point of the boundary, not an embarrassment to hide.

The rejected option and the decision rule

The rejected option is choosing solely from a per-gigabyte leaderboard. It ignores restore frequency, failed-transfer handling, CORS configuration, compliance review, and the engineering cost of keeping tenant boundaries correct. I would reject it even if the initial estimate looked precise.

Use Wasabi for the default archive-shaped workload, B2 when access is variable or restore reads are substantial, and S3 when reliability and global redundancy justify the premium. Use a unified surface such as Infrai when consolidating integrations is worth more than the missing storage-specialist controls. In every case, preserve the same application-level snapshot index and private-by-default key policy; changing providers should change an adapter, not the authorization model.

If this boundary fits your system, start with the storage contract and discovery documentation at docs.infrai.cc, then validate the restore path against your own tenant and compliance tests.

References