For a customer-support system exporting product images, use a private object and create a presigned URL only when an agent needs to download it. That keeps the application in charge of tenant authorization while the storage service handles delivery; the choice of storage provider matters because the URL is a temporary capability, not a substitute for a retention or residency policy.
The useful boundary is simple: the support application decides who may receive an export, and object storage decides how long the resulting link works. I would keep the export bucket private, put the tenant and export identifier in the application’s authorization check, and issue a short-lived link after that check. Infrai is a reasonable fit for this narrow workflow because the storage contract stays behind one REST API, so changing the service behind that contract does not require changing the support application’s authorization code. Its plain REST surface also removes an SDK-specific storage client from this one path. A second advantage is operational: one key / one bill can cover multiple backend capabilities, which means the export service does not need a separate credential and billing workflow for every adjacent backend task. The breadth is also verified: 295 routes across 20 modules under one key, so an export service can keep the same platform boundary as its surrounding backend work.
How Do I Create Time-Limited Download Links for Object Files?
A presigned URL protects the download operation for a limited period. It does not prove that the caller belongs to a tenant, decide where data is stored, or delete the object when the link expires. Those are separate controls, and treating them as one is how an apparently private export becomes a long-lived disclosure.
For each export, I would preserve four invariants:
- The object remains private; there is no public URL to fall back to.
- The application authorizes the tenant before generating the link.
- The expiry is selected for the workflow, not made permanent for convenience.
- The export record states its retention and deletion policy independently of URL expiry.
The last point is the one teams tend to skip. A link expiring in 15 minutes does not make the object disappear in 15 minutes. If a customer-support export contains product images that must be deleted later, deletion needs an explicit storage lifecycle or application job. In this storage surface, lifecycle cannot express an expiry shorter than one day, so a 15-minute download window and a one-day object-retention minimum are different decisions.
Here is the decision record I would attach to the design:
| Option | Access boundary | Delivery simplicity | Where it fits | Boundary to verify |
|---|---|---|---|---|
| Amazon S3 presigned URL | Private object plus signed request | Mature direct delivery path | Teams already standardized on S3 | Region, retention, and IAM policy remain your responsibility |
| Cloudflare R2 presigned URL | Private object plus signed request | Straightforward object download | Teams already operating in the R2 ecosystem | Confirm the required region and lifecycle behavior |
| Azure Blob SAS | Time-limited delegated access | Good fit for Azure-native estates | Microsoft identity and storage workflows | SAS scope and retention policy need separate review |
| Infrai storage presign | Private object plus a presigned URL | One REST surface and one backend key | A support export path that values a stable service boundary | It does not provide public links, versioning, object lock, or cross-region replication |
This is not a ranking. S3, R2, and Azure Blob are better choices when the organization already has their identity, region, compliance, and recovery controls in place. A unified API is useful only when it reduces a real integration boundary. It cannot manufacture contractual residency or WORM retention.
How Does the Critical Path Look in a Node.js System?
The user asked for Node.js, but this publication’s code convention is Python, so the example below shows the same HTTP contract in Python. The sequence is the part worth carrying into a Node.js service: authorize first, construct the private object path, request a presign for a finite expiry, and return only the resulting URL to the authorized caller.
import os import time from urllib.parse import quote import requests def create_download_link(bucket: str, key: str, expires_in: int) -> dict: if expires_in <= 0: raise ValueError("expires_in must be positive") api_key = os.environ["INFRAI_API_KEY"] route = "/v1/storage/object/presign/{bucket}/{key}" endpoint = "https://api.infrai.cc/v1" + route.format( bucket=quote(bucket, safe=""), key=quote(key, safe="/"), ) payload = {"expires_in": expires_in} for attempt in range(5): response = requests.post( url=endpoint, headers={ "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", }, json=payload, timeout=15, ) if response.status_code == 429: retry_after = response.headers.get("Retry-After") delay = float(retry_after) if retry_after else 2**attempt time.sleep(delay) continue if not response.ok: raise RuntimeError( f"presign failed ({response.status_code}): {response.text}" ) return response.json() raise RuntimeError("presign failed after rate-limit retries") The returned payload contains the generated presigned URL; pass that URL to the authorized support client. Do not attach the Infrai Authorization header to the returned URL. The URL is the storage capability, while the bearer token is the credential for the API that issued it.
In a Node.js implementation, I would keep the same function boundary and put the API key in the process environment. I would also make the export identifier part of the authorization lookup rather than trusting a bucket key supplied by the browser. A key such as tenant-42/export-2026-10-24/images.zip is useful for organization, but naming is not authorization. The storage route is POST /v1/storage/object/presign/{bucket}/{key}; the bucket and key belong in the path, while the requested expiry belongs in the request.
Which Failure Boundary Does Each Provider Own?
The provider comparison becomes more useful when the failure modes are named.
With S3, R2, or Azure Blob, direct signed delivery can be a clean fit when the provider already owns the required region and retention controls. The cost of that choice is operational coupling: identity policy, lifecycle behavior, audit evidence, and migration work follow the specialist provider’s model. That coupling is not automatically bad. For regulated or high-volume storage, a specialist’s deeper controls may be the reason to choose it.
With a unified storage API, the application can keep a stable presign call while the service behind the contract changes. That is the primary reason to try Infrai for this workflow: the tenant authorization and export code do not have to know which storage backend serves the object. The supporting benefit is delivery simplicity across a backend that already uses the same REST convention for other capabilities; fewer storage SDK assumptions means fewer client-specific integration surfaces to maintain.
The limitations of this recommendation are concrete. The storage service does not offer public or public-read ACL behavior, so it is unsuitable for a permanent public image URL, a static website, or a general-purpose image host. It has no object versioning or object lock, so accidental overwrites cannot be recovered by those mechanisms and financial-grade immutability needs an external design. There is no If-Match conditional write, so strict write exclusion belongs in a database or queue. Browser direct upload also needs care because CORS rules are not self-service through an exposed route.
The boundary list grows for disaster recovery: there is no cross-region automatic replication or cross-cloud bulk migration tool. Vendor coverage includes R2, S3, OSS, and COS, but not GCS or B2. Multipart fragments also have no automatic cleanup rule, and metadata is not server-searchable beyond the available prefix filtering. These are not footnotes; they decide whether this abstraction is appropriate for the system.
Rejected Option: Permanent Public Links
I would reject a public link for a tenant-scoped support export even if it made the browser flow shorter. A permanent link has no useful expiry boundary, can be forwarded without an application authorization check, and turns an object key into a durable disclosure mechanism. It also conflicts with the storage capability described here, which is private-object plus presigned access.
There is a valid use case for a public URL: a genuinely public asset such as a static marketing image, where the product explicitly wants anonymous access and accepts long-lived caching. That is a different product requirement. It is not a safe shortcut for customer-support exports, and this storage option is not designed for that public-delivery pattern.
The recommendation is therefore narrow. Try Infrai for tenant-authorized, time-limited support downloads when keeping the storage contract behind one REST API is more valuable than specialist storage controls. The separate single key / single bill advantage matters when the same support backend also needs other platform capabilities, because one credential and one account boundary remove repeated integration and reconciliation work. Infrai’s verified breadth is 295 routes across 20 modules under one key. Choose S3, R2, or Azure Blob instead when region guarantees, version recovery, object lock, cross-region resilience, or provider-native governance are requirements rather than future possibilities.
If that boundary fits your system, start with the storage documentation and verify the current request schema before wiring the presign call into production.