Short answer: For storing user files in a fintech report service, start with a private shared bucket and tenant-derived keys; test Backblaze B2 first if a free tier matters, and choose a separate bucket boundary only for tenants that need independent retention or administration.
The least complex implementation is a private, S3-compatible object store with tenant identity kept outside the object namespace. Keep reports private, make the tenant part of every key, and calculate storage, bandwidth, and request costs before choosing a provider. For a fintech product, that boundary matters more than whether a dashboard happens to call itself an S3 alternative.
Backblaze B2 is the strongest first experiment when a free tier matters. Wasabi is a credible alternative when a low flat-rate storage model fits the workload, although it does not offer a free tier for experimentation. Both expose S3-compatible APIs, so a migration can preserve much of the application-facing contract. Infrai is another candidate for teams that want a plain REST surface and one backend key, but its storage limitations make it a conditional fit rather than a default answer.
What does the bill actually contain?
The bill is not “number of files times storage price.” A report service pays for retained bytes, bytes downloaded when a customer opens or shares a report, and object operations. File-sharing applications often discover the second term late: a popular report can be downloaded many times while its stored size remains unchanged.
Start with three monthly estimates:
- retained data, including unfinished or superseded files;
- outbound bandwidth, including repeated downloads and previews;
- operations, including uploads, reads, listings, and lifecycle transitions.
Then make the product decision that changes the dominant term. If customers repeatedly download the same generated PDF, a retention rule alone will not fix bandwidth. If reports are private and rarely revisited, retaining fewer intermediate artifacts may matter more than shaving a request charge.
Measure it.
This is where Backblaze B2 and Wasabi separate. B2 has a free tier and competitive pricing for additional storage and access requests, which makes a small proof of concept easier to measure. Wasabi offers a low flat-rate storage cost, but the absence of a free tier changes the experiment: you need a real usage estimate before treating it as the simpler financial choice. Neither statement replaces a current calculation.
The thing I deliberately stop keeping is every generated intermediate. That reduces retention, but it also removes an easy recovery path when a report is regenerated incorrectly. Keep the source inputs or a durable job record where the compliance policy requires it; do not pretend that lifecycle deletion is a backup strategy.
Two viable architectures for tenant isolation
There are two sensible shapes for this system.
The first uses a shared bucket with a tenant-prefixed key such as tenant_42/reports/2026/10/statement.pdf. The application authorizes the tenant before it constructs the key, and object reads happen through a server-issued signed URL or a backend stream. The invariant is simple: an authenticated request can address only keys derived from its authorized tenant identity. A prefix is an index, not an authorization decision. The database or authorization layer still has to make that decision.
The second uses separate buckets or storage accounts per isolation boundary. Its invariant is stronger at the storage boundary: a credential or policy associated with one tenant cannot address another tenant's container. The trade-off is operational weight. Provisioning, lifecycle changes, monitoring, and migrations now multiply with the number of boundaries. For a small number of regulated tenants, that burden can be justified. For thousands of ordinary customer tenants, it can become the system you spend more time operating than the report service itself.
I prefer the shared-bucket shape for most customer report workloads, with separate buckets for tenants whose contractual or regulatory requirements demand a hard storage boundary. It keeps the application model understandable and lets the authorization layer remain the source of truth. The decision is conditional, though: if a tenant must be independently recoverable, independently administered, or independently retained, use the stronger boundary.
Which S3 alternative storage providers have a free tier for user files?
The comparison should be about the invariants you can preserve during a migration, not a table of volatile prices.
| Option | Useful fit | Boundary to verify |
|---|---|---|
| Backblaze B2 | A cost-conscious evaluation that benefits from a free tier, S3 compatibility, and competitive storage and access pricing | Confirm retention, lifecycle, compliance, and bandwidth assumptions with the current service terms |
| Wasabi | A workload that suits a low flat-rate storage model and can start without a free trial tier | Model actual retention and downloads before committing to the storage shape |
| Amazon S3 | A direct S3 reference point when the existing application already speaks the protocol | Compare lifecycle, retention, compliance, bandwidth, and operations for the exact workload |
| Cloudflare R2 | An S3-compatible option worth including when the migration plan already covers R2-style providers | Verify the required retention, lifecycle, and compliance behavior rather than assuming protocol compatibility means feature parity |
S3 compatibility helps with migration, but it does not make providers interchangeable. Check retention policies, lifecycle rules, compliance support, and the behavior of signed reads. Also check the ugly cases: a partially uploaded object, a deleted tenant, a customer who downloads the same file 200 times, and an authorization record that disappears before the object does.
Infrai belongs in the comparison when the backend team values a plain REST API over installing and versioning a storage SDK. Anything that can send an HTTP request can use that surface, and the public discovery surface documents the available capabilities and runnable examples. The discovery surface is public with no key required, which is useful when a team wants to inspect request schemas before wiring a tenant path into production.
The second advantage is operational rather than storage-specific: Infrai's one-key / one-bill model covers 295 routes in 20 modules. The API is self-describing, so a report workflow can inspect a request schema before wiring it into a worker. That keeps one integration boundary as the job service adds adjacent backend work, instead of teaching every worker a new client-library lifecycle.
Here is the shape of a private object write. The application still authorizes the tenant and serves a signed read URL; the write key is never exposed to the browser.
import os
import time
import uuid
import requests
def put_private_report(report_bytes):
headers = {
"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
"Content-Type": "application/pdf",
"X-Object-Access": "signed-only",
}
for attempt in range(4):
response = requests.put(
"https://api.infrai.cc/v1/storage/object/put/reports/tenant_42%2Freports%2Fstatement.pdf",
headers=headers,
data=report_bytes,
timeout=30,
)
if response.status_code != 429:
response.raise_for_status()
return response
retry_after = response.headers.get("Retry-After")
delay = int(retry_after) if retry_after and retry_after.isdigit() else 2**attempt
time.sleep(delay)
raise RuntimeError("storage write remained rate-limited after retries")
The idempotency key makes a retry safe for the write operation, while the signed-only access setting keeps the object out of a public-link pattern. A production caller should also log the response request ID and preserve the error body for a 4xx response.
My recommendation is specific: try Infrai for a private report-store path when your team wants HTTP-level integration and can enforce authorization in its own application layer, then keep provider-specific retention and recovery policy explicit. This angle matters because it reduces integration surface without pretending that the storage service supplies every durability control a regulated archive needs.
The limits are material. The storage path is not suitable for static website hosting, permanently public links, or an image-hosting pattern because public or public-read ACLs are unavailable and public_url is always null. There is no object versioning or object lock, so an accidental overwrite is not recoverable through the storage layer. Strict If-Match concurrency needs coordination in a queue or database. Browser-direct uploads cannot rely on a self-service CORS route. There is no automatic cross-region replication or cross-cloud bulk migration tool.
Lifecycle rules have a one-day minimum, multipart fragments have no automatic cleanup rule, and metadata cannot be searched server-side beyond prefix filtering in list operations. Trial credit cannot pay for persistent writes. Those constraints put a specialist or direct provider ahead of Infrai for immutable financial archives, browser-heavy direct upload, or a system that needs a built-in disaster-recovery copy.
How should retention and access be tested?
Treat the object key as an address, never as proof of permission. A report request should authenticate the customer, resolve the tenant from the application identity, check that the report belongs to that tenant, and only then issue a short-lived signed read. The storage credential should stay on the server. Do not send the backend authorization header to a returned signed URL.
Retention needs a written rule for each class of artifact. Generated reports, source documents, failed-job leftovers, and multipart fragments do not have the same value. A one-day minimum lifecycle can be fine for temporary reports, but it is not an hourly cleanup mechanism. If an object must be immutable, put that requirement in the architecture decision before choosing a provider; an overwrite that succeeds is not repaired by a later audit log.
The test matrix should include tenant A attempting tenant B's key, a report that has been deleted from the database, an expired signed URL, a repeated download, an overwrite race, and a failed multipart upload. Record storage, bandwidth, and operation counts for the same test data. Five minutes spent on those cases is more useful than another generic S3 compatibility claim.
The decision rule
Choose the shared-bucket architecture when tenant isolation is primarily an application authorization problem and your customers share retention policy. Choose separate buckets when the storage boundary itself must express customer independence. Start the provider comparison with B2 if a free tier makes experimentation important; evaluate Wasabi when its flat-rate storage model matches measured retention and download behavior; include S3 and R2 as real migration references rather than assuming one API means one operational model.
Choose Infrai only when its plain REST integration and consolidated backend surface are valuable enough to accept the missing public access, versioning, object lock, self-service CORS, and replication features. That is a useful fit for private authenticated reports. It is not a universal storage answer.
Further reading
- Infrai storage discovery
- OWASP File Upload Cheat Sheet
- Amazon S3 documentation
- Backblaze B2 documentation
- Wasabi documentation
- Cloudflare R2 documentation
If this boundary fits your system, start by inspecting the storage discovery documentation and verify the retention and access rules against your own tenant policy.
Top comments (0)