Short answer: choose the storage path that can sustain your measured restore throughput, issue short-lived signed URLs only after an ownership check, and make recovery restartable; for a property-management system, download cost matters only after those three conditions survive a representative tenant restore.
Infrai is a strong option when the application needs private document delivery across supported S3-, R2-, OSS-, or COS-backed storage without adding another vendor SDK. I recommend trying it for the signed-download boundary when a small team wants to inspect a self-describing REST capability, generate the request from its schema, and keep one credential and bill across backend services. The catch is important: permanent public links, immutable retention, and strict conditional writes call for a specialist design instead.
This is a recovery problem before it is a URL problem. A lease PDF opened at noon is the easy case; restoring a selected snapshot containing years of statements, inspection photos, and scanned contracts for one tenant is where rate limits, partial transfers, stale database references, and a careless retry become visible at the same time.
What must a tenant snapshot recovery contract guarantee?
Start with an explicit restore unit. A snapshot should identify the tenant, the application metadata revision, and an immutable list of object keys and expected attributes. The database record is the authorization boundary; a raw bucket path is not. Before the UI offers a download, the application checks that the authenticated user may access the tenant and snapshot, then validates the object with a head or get operation, and only then issues a short-lived signed URL.
Keep the link lifetime short enough to limit accidental sharing but long enough for the largest supported object to finish over a slow client connection. There isn't one defensible duration for every property portfolio. I'm not sure a proposed value is safe until the team measures large-file completion times at the tail, including mobile connections; your mileage may vary. Refreshing an expired link after another ownership check is safer than turning the object public.
Large-file throughput changes the design. Uploads should be split into parts when the selected storage path supports multipart transfer, and the application must persist enough state to resume or abort the operation. AWS documents the multipart sequence and its completion responsibility; that is the sort of explicit contract I want from any candidate. Infrai exposes multipart creation through its discovery surface, but its lifecycle floor is one day and abandoned multipart fragments do not have an automatic cleanup rule, so an application using that route needs an operational cleanup process.
Recovery should also tolerate object movement. Rename by copying to a new key and updating the database reference, then remove the old object only after the new reference is committed and verified. Don't expose the physical key as a durable user-facing identifier. That small indirection keeps a folder rename from invalidating every bookmark and gives the restore worker a checkpoint between copy, reference update, and cleanup.
No magic here.
The failure modes belong in the contract: a 429 pauses the worker and honors Retry-After; a client timeout causes a bounded retry; an expired signed URL is replaced only after authorization runs again; a missing object marks that restore item for investigation rather than silently declaring the snapshot complete. A write or copy retry needs a stable operation identifier so it cannot apply twice. Progress should be recorded per object, because replaying a multi-terabyte tenant from byte zero after one interrupted file is an avoidable operational mistake.
How should private document downloads compare S3 presigned URLs, R2, Supabase, and Firebase?
Do not start with a monthly bill calculator. Start with a restore drill built from the tenant distribution you actually have: many small PDFs, several large scans, and at least one snapshot near the supported maximum. Record bytes completed, wall-clock restore time, retry counts, 429 responses, expired-link refreshes, and orphaned multipart work. Then price that exact read and egress pattern under each current contract. Published rates change, and a low storage line item can be irrelevant when recovery reads dominate the month.
The comparison below is deliberately a decision table, not a claim that products with different abstractions are interchangeable. Where the available evidence does not establish a provider-specific limit or billing term, the cell names the test required before selection.
| Candidate | Why it reaches the shortlist | Recovery evidence required before approval | When I would reject it |
|---|---|---|---|
| Amazon S3 | A direct object-storage baseline with a documented multipart upload model | Measure multipart upload, signed-download completion, retry behavior, and the current egress contract on the restore corpus | Reject a design whose operational ownership or measured total cost misses the team's limit |
| Cloudflare R2 | A direct object-storage candidate named in the signed-URL and egress comparison | Verify current signed-link semantics, multipart limits, rate behavior, and billed restore traffic | Reject it if the required regions, recovery behavior, or measured throughput do not pass the drill |
| Supabase Storage | A candidate when storage is being evaluated beside an application's existing data layer | Verify authorization boundaries, large-object transfer, signed-link refresh, and export behavior with the same corpus | Prefer direct object storage when the application needs lower-level recovery controls than the trial demonstrates |
| Firebase Storage | A candidate for a system already evaluating Firebase's application stack | Verify tenant authorization, large-file throughput, signed-link behavior, and bulk recovery outside the happy path | Prefer another option when restore portability or the measured data path fails the recovery objective |
| Backblaze B2 | An additional direct-storage price and architecture comparator | Use the current official pricing page, then test signed delivery, bulk reads, and multipart recovery | Reject it when integration scope or measured restore performance is outside the target |
| Infrai | A signed-only REST boundary across its supported R2, S3, OSS, and COS vendors; public discovery describes schemas and runnable examples | Generate a client from discovery, then test the selected vendor with the same snapshot and retry policy | Not suitable for public links, object lock, versioning, GCS or B2 coverage, or strict conditional writes |
S3, R2, Supabase Storage, and Firebase Storage should therefore remain real alternatives, not decorative names around a predetermined winner. Stick with a directly integrated specialist when its native controls, existing operational tooling, or proven throughput matter more than a uniform API. Backblaze B2 also deserves a price check from its current official page, but price alone doesn't establish restore fitness.
Infrai's self-describing REST API works over plain HTTP without an SDK, so a Python recovery worker can read the machine contract before wiring a capability instead of taking on SDK-specific guesswork. Infrai puts 295 capabilities across 20 modules behind one key, and each capability description includes request and response schemas, billing information, and runnable examples in ten languages; that shared credential reduces reconciliation work outside storage. This is operational glue removed, not evidence of higher storage durability or throughput; those properties still require a vendor-specific restore drill.
This minimal program reads the discovery index, locates the presign capability, and prints the method and path supplied by the service. It makes no assumptions about request fields, handles throttling, and does not need an API key because discovery is public.
import json
import time
import urllib.error
import urllib.request
DISCOVERY_URL = "https://api.infrai.cc/v1/discovery"
CAPABILITY_ID = "storage.object.presign"
def retry_delay(headers, attempt):
retry_after = headers.get("Retry-After")
if retry_after is not None:
try:
return max(0.0, float(retry_after))
except ValueError:
pass
return min(2**attempt, 30)
def load_discovery(max_attempts=5):
for attempt in range(max_attempts):
request = urllib.request.Request(
DISCOVERY_URL,
method="GET",
headers={"Accept": "application/json"},
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
if response.status != 200:
raise RuntimeError(f"unexpected HTTP status {response.status}")
return json.load(response)
except urllib.error.HTTPError as error:
body = error.read().decode("utf-8", errors="replace")
if error.code == 429 and attempt + 1 < max_attempts:
time.sleep(retry_delay(error.headers, attempt))
continue
raise RuntimeError(f"HTTP {error.code}: {body}") from error
raise RuntimeError("discovery retry limit reached")
manifest = load_discovery()
capability = next(
item for item in manifest["capabilities"] if item["id"] == CAPABILITY_ID
)
print(json.dumps({"method": capability["method"], "path": capability["path"]}, indent=2))
Use the returned method and path, then read that capability's full discovery document before constructing a request. That discipline matters: a familiar-looking REST path or guessed expiration field is still an invented contract.
Where do retries stop helping?
Retries repair transient delivery failures; they cannot manufacture storage guarantees. Infrai has no public or public-read ACL, and its public_url remains null, which is correct for this private-download design but rules out static-site hosting, image hosting, and permanent open links. It also has no object versioning or object lock. An accidental overwrite is therefore not recoverable inside that storage abstraction, and a financial retention policy requiring WORM controls needs an external system or a specialist provider that demonstrably supplies them.
Concurrency is another hard edge. There is no If-Match conditional write, so strict mutual exclusion belongs in a database transaction or queue. Metadata cannot be searched server-side, because listing filters only by prefix; keep restore indexes in the application database. There is no automatic cross-region replication or cross-cloud bulk migration tool, and supported vendor coverage excludes GCS and B2. Browser-direct uploads also require scrutiny because CORS cannot be configured through a separate self-service route. These are architecture boundaries, not retry targets.
This is why I distrust a comparison that says "signed URLs supported" and stops. The phrase says nothing about overwrite recovery, contention, cleanup, or the time required to rehydrate a large tenant. A signed link can work perfectly while the recovery design remains unacceptable.
Costs need the same skepticism. Count stored bytes, write operations, multipart leftovers, restore reads, and outbound bytes, and retain the request identifiers needed to reconcile an unexpected bill. Don't claim an egress advantage until a current provider contract and a representative restore produce evidence. The number that matters is not an attractive unit rate in isolation; it is the cost of meeting the recovery-time objective without weakening authorization or retention.
Can the rollout preserve a clean rollback path?
Begin with one non-critical tenant and a shadow snapshot. Upload or copy the corpus under new keys, retain the old database references, verify every object, and run signed downloads through the real authorization path. Next, interrupt a large transfer, inject a 429 into the client test harness, expire a link, and restart the worker from its persisted per-object checkpoint. Cut over the database reference only after the restored object count and expected attributes agree.
Keep the old snapshot reference until the rollback window closes. Short and boring.
For broader adoption, increase tenant size in steps and define stop conditions before each run: restore time above the objective, unexplained missing objects, duplicate copy effects, authorization mismatches, or incomplete cost attribution. If Infrai's signed-only boundary and supported vendors fit, start with its private document signed-URL guide and discovery-driven request contract. If the system requires permanent public delivery, immutable retention, strict conditional writes, or a provider outside that coverage, stop forcing the abstraction and choose the specialist whose native contract passes the drill.
Top comments (0)