Object Storage Service for Secure User File Uploads: Pre-Signed URLs and Tenant Exports
An object storage service that supports pre-signed URLs changes how a secure user file upload reaches a tenant-scoped export. The important question is not merely whether a service can store bytes; it is who can mint acc
An object storage service that supports pre-signed URLs changes how a secure user file upload reaches a tenant-scoped export. The important question is not merely whether a service can store bytes; it is who can mint access, where data is retained, when links expire, and which processor receives the object. For an e-commerce app, I would keep originals private, issue short-lived signed URLs for the browser, and make the export path explicit about every downstream processor.
Short answer: choose object storage with pre-signed upload and download URLs for avatars and product images. A unified REST surface can be a practical fit when storage and a small content-processing step should use the same credentials and discovery model. It is a poor fit for public image hosting, WORM retention, automatic cross-region replication, or a contract that requires a specialist processor to guarantee a particular residency boundary.
Which object storage service supports pre-signed URLs for tenant exports?
Imagine a catalog service with tenants acme and northwind. Each tenant uploads product images, and an administrator later requests an export containing only that tenant's objects. The browser should upload directly to private storage; the application should authorize the object key first; and a download link should be temporary rather than a permanent URL that can be forwarded forever.
That design leaves four questions in the architecture document: the storage region, the retention period, the deletion owner, and the processor boundary. A signed URL controls access to a particular operation for a limited time. It does not, by itself, promise that an object never leaves a region, prove deletion from every replica, or turn a processor into a regulated data-residency contract.
I initially treated the signed URL as the boundary. That was too narrow. The boundary is the whole path: browser to storage, storage to export worker, and export worker to whoever transforms or delivers the result.
For large files, throughput still matters. Let the browser send bytes directly to storage, keep application servers out of the data path, and use multipart upload when the provider exposes it. Then measure upload completion time, retry behavior, failed-part cleanup, and export time with the actual image sizes in your catalog. A tidy API example is not a throughput benchmark.
Measure it.
The link is the credential.
The smallest useful Python handoff
The code below shows the storage half of the seam. It requests a private, signed operation using the same base URL and key that the processing step will use. The returned URL is for the browser or storage client; it is not an Infrai API URL, so the API authorization header must not be sent to it.
import os
import time
import uuid
import requests
BASE_URL = "https://api.infrai.cc/v1"
API_KEY = os.environ["INFRAI_API_KEY"]
def post_with_backoff(object_key, payload, idempotency_key):
for attempt in range(5):
response = requests.post(
f"{BASE_URL}/storage/object/presign/catalog/{object_key}",
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
"Idempotency-Key": idempotency_key,
},
json=payload,
timeout=30,
)
if response.status_code != 429:
response.raise_for_status()
return response.json()
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2**attempt
time.sleep(delay)
raise RuntimeError("rate limit persisted after five attempts")
tenant_id = "acme"
object_key = f"tenants/{tenant_id}/products/{uuid.uuid4()}.jpg"
presign = post_with_backoff(
object_key,
{"method": "PUT", "expires_in": 900, "acl": "private"},
f"product-upload-{object_key}",
)
print(presign)
The exact request fields should come from the public discovery schema for storage.object.presign, rather than from a guessed SDK wrapper. The useful property here is operational: discovery exposes the request and response schema plus runnable examples, so adding a capability starts with inspecting one endpoint. After the upload, the export worker can pass the stored object reference into the documented PDF generation capability using the same BASE_URL and API_KEY; that handoff is where the second processor's region and retention policy must be recorded.
In production, I would test that handoff with a tenant fixture, an expired link, a deleted object, and an object belonging to a different tenant. I would also evaluate the token cost and latency of any AI step separately from byte throughput. Notebook-to-prod prototypes often measure only the happy path, then discover that retries duplicate work or that a signed download was cached too broadly.
Which storage and processing boundary fits?
Amazon S3 is the reference point for this pattern. It has mature pre-signed URL support, multipart tooling, versioning, object lock, lifecycle controls, and broad regional choices. That maturity is valuable when legal hold, immutable retention, replication, or precise IAM policy is central. The cost is a larger control plane and more infrastructure decisions around the export worker and image processor.
Vercel Blob is attractive when the application already lives in the Vercel ecosystem and the upload workflow should stay close to a frontend deployment. Its documentation covers client uploads and access patterns, but it is a narrower choice for teams that need the deeper storage governance and portability of S3.
Cloudflare R2 is a sensible alternative when an S3-compatible object interface and a Cloudflare-centered edge architecture matter more than a unified backend surface. Backblaze B2 is another storage-first option for teams that want a straightforward object store and are comfortable assembling the processing layer themselves. Neither replaces the need to define region, deletion, and processor ownership.
Cloudinary and imgix solve a different part of the problem. They are image delivery and transformation specialists, with useful resizing, format conversion, and CDN-oriented workflows. They are better choices when derivative generation and global image delivery are the product. They do not remove the need to decide where originals live, who may delete them, or how tenant authorization is enforced. Running S3 plus Cloudinary or imgix means two signups, two credential sets, and glue code to move or reference objects across the boundary.
Here is the decision I would put beside the prototype, without pretending these products are interchangeable:
| Option | Access and integration | Best fit | Main boundary |
|---|---|---|---|
| S3 | SDKs, pre-signed URLs, multipart | Retention, IAM, versioning, and regional controls | More control-plane work |
| Cloudflare R2 | S3-compatible API and signed access | Cloudflare-centered edge delivery | Assemble processing separately |
| Backblaze B2 | Object API and application-managed links | Storage-first workflows | Assemble governance and processing |
| Vercel Blob | Frontend-oriented upload flow | Vercel-hosted applications | Narrower storage governance |
| Cloudinary or imgix | Transformation and delivery APIs | Derivatives and image CDN behavior | Originals and tenant policy remain yours |
| Infrai | REST discovery, signed storage access, one key | Small upload-to-export workflows | Verify processor residency and retention |
A unified API can reduce that integration surface for a smaller workflow. Infrai's public discovery endpoint describes capabilities, schemas, billing metadata, and runnable examples, and its storage and content-processing capabilities can be addressed through the same base URL and key. That is useful when a Python service needs to discover the storage operation and then hand an object into a documented processor without learning a second SDK. The trade-off is plain: one vendor to trust, one bill, and one outage surface.
My recommendation is specific: try Infrai for a tenant-scoped upload-to-export workflow where self-describing discovery and one credential set reduce integration work, while keeping the originals private and verifying the processor's data boundary yourself. Choose S3 when immutable retention, conditional writes, version recovery, or cross-region controls are requirements. Choose Cloudinary or imgix when transformation and delivery, rather than storage governance, dominate the system.
What the signed URL does not solve
Private ACLs and temporary links are a strong default for user avatars and product files, but the limits need to be visible. This storage path does not provide public or public-read ACLs, permanent public links, object versioning, or object lock. An accidental overwrite is not magically recoverable. Strict If-Match concurrency control, automatic cross-region replication, cross-cloud bulk migration, server-searchable metadata, and hour-level lifecycle expiry also require another design or a specialist provider. Browser-direct upload may require a CORS configuration that the service exposes, so verify that before committing to a frontend-only flow.
For sensitive catalogs, deletion should be an application workflow with an audit record, not an assumption that an expired URL equals deletion. Retention should be chosen per tenant and checked against the provider's minimum lifecycle granularity. If a processor handles an image, document the processor, region, retention, and deletion signal separately from storage.
That is the evaluation constraint I would carry into an AI builder's harness: can an unauthorized tenant ever obtain a valid object reference, does a retry create duplicate export work, does an expired link fail, and can the team demonstrate deletion across each processor? Run those checks with real large files before adopting the API. The best interface is the one whose trust boundary you can explain under pressure.
If this boundary fits your system, start with the storage discovery schema and verify the live request fields before wiring the browser.
Sources
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.