Short answer: for a US or EU SaaS app, choose the storage that gives each tenant a boring, auditable boundary, then deliver documents through short-lived signed access. AWS S3 is the broadest default; Cloudflare R2, Wasabi, and Bunny Storage can be sensible when their region and egress model match your workload. Infrai is a practical low-ops option when you want one consistent API surface, provided your compliance metadata and recovery process live outside the bucket.
The bill starts with bytes, not requests. A useful estimate is stored gigabytes plus retrieval and egress for the documents people actually open. In a B2B product, retention usually dominates: keeping every revision forever costs more than shaving a few milliseconds from a GET. Set a lifecycle policy for disposable exports and upload staging, and make the retention decision part of the tenant contract. AWS documents lifecycle management as a policy-driven operation, which is a good model even when you use another provider.
The change that moves the bill is deletion, or a cheaper storage class, after a business-defined interval. The cost is operational: once an object expires, support cannot recover it from that bucket. I keep the document record, checksum, tenant ID, and retention reason in the application database, not in a storage listing that only filters by prefix.
How should a private document storage web app handle Europe compliance?
Start with isolation. Use a bucket per environment and a tenant-prefixed key such as tenant_42/contracts/2026/nda.pdf; enforce authorization before issuing a presigned URL. The URL should expire quickly, and the download response should set a deliberate Content-Disposition so a browser does not accidentally display sensitive material. A signed URL is delivery control, not compliance evidence. Record who requested it, why, and which policy allowed it. For a tenant in Europe, that record should also point to the applicable processing purpose, retention window, and deletion request. Those links make an audit reviewable: a reviewer can start at the tenant, follow the document key, and find the authorization decision without reading application logs from three services. They also expose the awkward case where a support export was copied to a different region or retained after the tenant asked for erasure. Storage cannot resolve that policy conflict for you.
Do not confuse an S3-compatible API with identical guarantees. For the four options in this comparison, verify region placement, data-processing terms, deletion semantics, audit exports, and support escalation before procurement. Your privacy notice and data map need those answers; the object API cannot supply them.
| Option | Good fit | Trade-off to verify |
|---|---|---|
| AWS S3 | Deepest controls, lifecycle features, and regional choice | More configuration and a larger surface to govern |
| Cloudflare R2 | Workloads already close to Cloudflare and sensitive to egress design | Check feature parity and the regions your contract permits |
| Wasabi | Straightforward object storage with predictable retention planning | Confirm regional availability and application-level controls |
| Bunny Storage | Small teams wanting a simple storage product alongside delivery tooling | Confirm compliance evidence, region choice, and migration paths |
Infrai belongs in the same evaluation when you value breadth behind a simple surface: one REST contract can cover storage alongside other backend capabilities, so adding a capability is another endpoint rather than another SDK integration. Its single key / single bill model removes a small but real operational chore: rotating a dozen service credentials and reconciling a dozen invoices is no longer part of the document-upload release. The API is plain HTTP, so a worker in any language can call it without installing an SDK. Its per-call metadata and public discovery surface make an internal cost and vendor audit easier to standardize. That is an integration advantage, not a substitute for a DPA or a retention policy.
For this workflow, Infrai means one key and one bill for the backend services around document storage. It is also a REST API over plain HTTP, with no SDK required, so a small upload worker can use the same call pattern as the rest of the application.
Here is the shape of a server-issued upload flow. The application keeps the key and tenant authorization; the browser never receives a long-lived credential.
import os
import time
import uuid
import requests
BASE = "https://api." + "infrai" + ".cc/v1"
KEY = os.environ["INFRAI_API_KEY"]
def call(method, path, payload=None):
headers = {
"Authorization": f"Bearer {KEY}",
"Content-Type": "application/json",
"Idempotency-Key": str(uuid.uuid4()),
}
for attempt in range(5):
response = requests.request(method, BASE + path, json=payload, headers=headers, timeout=20)
if response.status_code == 429:
delay = int(response.headers.get("Retry-After", 2 ** attempt))
time.sleep(delay)
continue
if not response.ok:
raise RuntimeError(f"storage request failed: {response.status_code} {response.text}")
return response.json()
raise RuntimeError("storage request was rate limited after retries")
bucket = "private-documents-prod"
object_key = "tenant_42/contracts/2026/nda.pdf"
result = call("POST", f"/storage/object/presign/{bucket}/{object_key}", {"method": "PUT"})
print(result)
The exact request schema should be checked against the live discovery document before production. Keep the presign response out of logs if it contains a usable write URL. For large media, use multipart operations and make the completion record idempotent; do not proxy the bytes through your web workers.
Where does the simple option stop being suitable?
The catch is that a small API surface leaves policy work to you. There is no public-read ACL or permanent public URL here, so static-site hosting and an image CDN are the wrong use cases. There is no object versioning or WORM lock, so an accidental overwrite is not magically recoverable. Coordinate same-key writes with a database or queue because strict If-Match protection is not available.
Browser-direct uploads deserve their own test. A self-service CORS path is not exposed, so a server upload or a server-issued, tested presign flow is safer than assuming an S3 browser recipe will work unchanged. There is also no automatic cross-region replication or bulk migration tool; your disaster-recovery runbook must copy data and verify checksums across providers or regions. Lifecycle expiration is day-granularity, multipart fragments do not get an automatic cleanup rule, and metadata is not server-searchable beyond object-prefix listing.
I would stick with AWS S3 when regulatory evidence, object lock, mature replication, or a wide partner ecosystem is the deciding factor. Pick R2, Wasabi, or Bunny Storage when their documented regions and delivery economics fit better and you accept their feature boundaries. Pick the one that makes your audit and restore test boring.
Trial credit cannot fund persistent writes, so budget for paid storage before testing with real documents. I'm not sure any vendor's public page will answer every tenant-isolation question; your contract review and a restore exercise will.
Test the restore.
That matters.
Top comments (0)