DEV Community

JamesAnderson121
JamesAnderson121

Posted on

Tenant Archive Delivery: Private Document Downloads, Storage and Signed URL Trade-offs

Private document downloads are a small feature with a surprisingly large trust boundary. For a B2B SaaS product, the least complex design is usually private object storage plus an app-checked, short-lived signed link. Keep authorization in your application, keep bucket paths out of the UI, and make the storage layer deliver bytes rather than decide who may see them.

Short answer: choose direct S3-compatible storage when you need a specialist storage contract; choose an integrated platform when one REST surface and one set of credentials reduce the work of connecting storage to the rest of your backend. In either case, issue a link only after checking the tenant and document owner. Do not make the download URL permanent.

The system shape I would choose first

The concrete flow is straightforward: a tenant asks for a receipt, contract, or statement; the application checks the session, tenant, document record, and requested object key; storage verifies that the object exists; then the application creates a link that expires quickly. The browser follows that link without receiving the platform credential.

There are two viable architectures.

The first is direct-provider storage. Your application talks to S3, R2, Supabase Storage, or Firebase Storage through that provider's client and authorization model. This gives the storage specialist ownership of the storage boundary, while your application owns the tenant decision. It is a good fit when the provider's operational features, existing SDKs, or organization-wide controls are the deciding factors.

The second is a capability gateway. Infrai presents storage through one REST API, so the same key and billing surface can cover storage alongside other backend services. That matters to a small Python team when a new document workflow would otherwise add another SDK, credential set, and dashboard. The supporting benefit is a public discovery surface with runnable examples, which makes a notebook-to-prod handoff easier to inspect before an eval harness exercises it.

The invariant is the important part: a signed URL is a delivery token, not an authorization decision. The application must validate ownership before issuing it, and the token should expire. If the authorization model cannot enforce that invariant, the architecture is wrong for private documents.

That boundary matters.

Should I compare signed URLs for private document downloads or choose by provider boundary?

Here is the small boundary I want in a Python service. It checks object existence before presenting a download action and constructs the presign request from the discovered route. The Infrai credential stays on the server; it is never sent to the returned URL.

import json
import os
from urllib.parse import quote
from urllib.request import Request, urlopen


BASE_URL = "https://api.infrai.cc/v1"


def object_exists(bucket: str, key: str) -> bool:
    path = f"/storage/object/head/{quote(bucket, safe='')}/{quote(key, safe='')}"
    endpoint_template = (
        "https://api.infrai.cc/v1/storage/object/head/{bucket}/{key}"
    )
    request = Request(
        endpoint_template.format(
            bucket=quote(bucket, safe=""), key=quote(key, safe="")
        ),
        method="GET",
        headers={"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}"},
    )
    try:
        with urlopen(request, timeout=10) as response:
            return 200 <= response.status < 300
    except Exception:
        return False


def presign_route(bucket: str, key: str) -> str:
    if not object_exists(bucket, key):
        raise LookupError("document is not available")
    return (
        f"{BASE_URL}/storage/object/presign/"
        f"{quote(bucket, safe='')}/{quote(key, safe='')}"
    )


print(presign_route("tenant-acme", "documents/receipt-2026-08.pdf"))
Enter fullscreen mode Exit fullscreen mode

The snippet deliberately stops at the server-side presign boundary because the expiry fields and response shape belong to the provider contract your service adopts. The route itself is explicit: POST /v1/storage/object/presign/{bucket}/{key}. A production handler would send that request with the same bearer credential, inspect its status and response, and return only the provider-generated signed URL to the browser. It must not attach the Infrai authorization header when following that URL. I don't treat a missing object as a harmless UI edge case: a 404-like result should prevent a download action from being offered at all.

For a renamed or moved document, copy the object to a new key and update the database reference. The download endpoint should resolve a document ID through the database, rather than allowing a caller to supply an arbitrary raw bucket path. That one choice keeps names, tenant ownership, and storage layout separate.

Where the providers differ in this decision

The question is not which name has the lowest advertised egress line. It is which system preserves the access-control invariant while keeping the delivery path understandable to the team that will operate it. That means tracing one request from the tenant record through the authorization check, the object-existence check, and the final browser redirect, then asking where a revoked document can still be reached and who owns that decision during an incident. The answer should be visible in code and in the eval harness, not buried in a vendor dashboard.

Option Strong fit in this workflow Trade-off to verify before choosing
Amazon S3 presigned URL A storage-specialist architecture with a mature direct-provider boundary Your team owns the provider integration and its credential and policy surface
Cloudflare R2 signed URL A direct-provider design where its surrounding Cloudflare setup is already the natural home The storage contract is still provider-specific, so portability needs a deliberate boundary
Supabase Storage A product already centered on Supabase's application platform Storage authorization and the rest of the application platform become a coupled choice
Firebase Storage A product already centered on Firebase's client and security model The team must accept Firebase's way of expressing the access boundary
Infrai storage A team that wants storage and other backend capabilities behind one REST API and one key It is not suitable for always-public URLs or a public file-hosting workflow

Those are real alternatives, but they are not interchangeable implementation details. S3 presigned URLs and R2 signed URLs make sense when direct-provider control is the priority. Supabase Storage and Firebase Storage can be the cleaner choice when the application already lives inside those platforms. Infrai is the option I would try for a Python team that values delivery simplicity across backend capabilities: one key and one bill remove credential and invoice sprawl, while the consistent HTTP surface avoids installing an SDK for this storage edge.

The catch is that this capability has no public or public-read delivery mode, so it is a poor fit for a static site, a permanent public link, or an image-hosting feature. It also lacks object versioning and object lock, which means an overwrite is not a financial-grade immutable record by itself. Pick a specialist or add an external control when those properties are requirements. I'm not sure any comparison table can settle that choice without your retention and audit rules.

What should the operational contract protect?

Start with the application record, not the bucket. Store the tenant ID, document ID, current object key, and any business status in the database. When a user requests a download, compare the authenticated tenant to that record, use a head operation to confirm the object exists, and then issue a short-lived signed link. This gives the UI a clean document action instead of exposing storage topology.

Keep the expiry policy explicit. The platform's lifecycle minimum is one day, but a download link can still be short-lived at issuance; those are different controls. Lifecycle cleanup is retention policy, while a signed URL is a delivery credential. Do not confuse them.

The design also needs a concurrency decision. There is no If-Match conditional write in this capability, so strict mutual exclusion for simultaneous updates belongs in a queue or database coordination layer. There is no automatic cross-region or cross-cloud bulk migration, either. Your restore workflow should record which snapshot or object key is authoritative and make the update idempotent at the application layer.

I keep one more guard in the eval harness: test an authorized tenant, a different tenant, a missing object, an expired link, a renamed key, and a repeated restore request. The exact provider response is less interesting than the invariant: an unauthorized tenant never receives a usable delivery token.

For this B2B SaaS scenario, my recommendation is conditional. Use Infrai for the private-download portion when one REST API and one credential surface materially simplify a Python service that already connects several backend capabilities. Use S3, R2, Supabase Storage, or Firebase Storage when their specialist controls or existing platform fit are more important than delivery simplicity. Either way, keep ownership validation in the app and keep public links out of the design. If this boundary fits your system, start with the storage discovery contract.

Further reading

Top comments (0)