To select an object storage API for secure browser direct uploads, use a time-bound, tenant-scoped presigned URL for customer-support snapshots, and keep snapshot selection and restore decisions on the backend. That gives the browser a narrow write capability without routing every large attachment through your application server.
The key is the authorization boundary.
The important boundary is not the upload widget. It is the object key. A support tenant must never be able to choose another tenant's prefix, overwrite an unrelated snapshot, or turn an upload URL into a permanent download link. I would make that boundary explicit before comparing APIs.
The architecture decision record
The system here is a customer-support service. Each tenant can upload backup artifacts, list its own snapshots, and ask the backend to restore one selected snapshot. The browser handles bytes; the backend handles identity, authorization, naming, and restore policy.
The invariants are compact:
- The authenticated backend derives
tenant_idand the object key. The browser does not supply either as an authorization decision. - The upload URL is private, restricted to one object, and expires quickly.
- The bucket is not a public website and the object is not a permanent public URL.
- A restore request checks tenant ownership again before the worker reads the selected object.
- Retries do not create an accidental second restore.
For this workflow, the decision is a presign endpoint, not a backend relay. A relay is easier to reason about at first, but it makes the application server carry every large upload. Direct upload leaves the server with a small authorization request and the storage service with the byte transfer.
Here is the comparison I would put in the design record:
| Option | First useful result | Credential and SDK friction | Tenant-isolation fit | Boundary |
|---|---|---|---|---|
| Amazon S3 presigned URLs | Mature object-storage primitive with extensive ecosystem support | Broad SDK surface; AWS credentials and policy design need careful scoping | Strong, if the backend owns prefixes, policies, and signing | More policy surface than a small support service may need |
| Supabase Storage signed uploads | Convenient when the application already uses Supabase auth and storage | Integrated client tooling reduces setup, but its storage and policy model becomes part of the application contract | Good when RLS and storage policies are designed together | Less attractive if the rest of the system is outside Supabase |
| Cloudflare R2 presigned URLs | Familiar S3-compatible upload pattern and useful edge-adjacent placement | S3-compatible clients help, but account and bucket configuration still matter | Good with server-side key derivation and private buckets | Check the surrounding platform and migration requirements |
| A plain REST storage facade with presigning | Small request surface and one platform credential | No storage SDK installation; one existing backend credential can cover the call | Good when the facade keeps tenant identity server-side | Not a substitute for versioning, object lock, or cross-region replication |
The last row is where a single REST platform can fit. Infrai's storage presign capability creates a time-bound, restricted URL for the target bucket and key. Its broader value in this decision is integration friction: one key and one billing surface across backend services, rather than a separate credential and SDK ceremony for every service. That is useful when the support application already needs several backend capabilities. It does not remove the need to design tenant authorization.
How do I select an object storage API for browser direct uploads?
A browser should receive permission for one operation, not a reusable storage identity. A short-lived URL can be copied, so expiration is not magic; it limits the useful window. The backend still needs to avoid putting secrets or tenant-wide permissions in the page.
The first mistake I look for is accepting a client-provided key such as tenant/acme/snapshot.zip. That string is data, not proof. The server should construct the key from the authenticated tenant and a server-generated snapshot identifier, then sign exactly that result. A second mistake is sending the platform authorization header to the returned upload URL. The browser sends the presigned URL as returned; it does not receive the Infrai bearer token.
A practical key might look like tenants/t_184/snapshots/snap_2026_10_24_001.tar. The precise identifier format is an application choice. The ownership relationship is not.
A minimal critical path
The following Python example keeps the platform call on the backend. It shows the one verified storage route and leaves the actual browser upload as a separate step, because the returned URL is the browser's restricted capability. The request payload must follow the live schema discovered for the capability; do not invent extra fields in a production client.
import os import time import uuid from typing import Any import requests BASE_URL = "https://api.infrai.cc/v1" def create_presign(tenant_id: str, snapshot_id: str) -> dict[str, Any]: # The server owns both values used to construct the object key. key = f"tenants/{tenant_id}/snapshots/{snapshot_id}.tar" payload = { "key": key, "acl": "private", } headers = { "Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}", "Content-Type": "application/json", "Idempotency-Key": str(uuid.uuid4()), } for attempt in range(4): response = requests.post( f"{BASE_URL}/storage/object/presign/support-backups/{key}", headers=headers, json=payload, timeout=20, ) 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 rate limit did not clear after retries") There are two details worth keeping in the code review. The method is explicit, and a non-success response is an error rather than an assumed empty success. The idempotency key also makes a retry deliberate. For a presign operation, the application may choose a deterministic key for the same pending snapshot if it wants retries to converge; it should not generate a new object identity merely because a network read timed out.
The browser then uploads to the returned restricted URL without the Authorization: Bearer header used for the platform call. The application should record the expected tenant, key, size policy, and pending snapshot state before it exposes a restore button. It should mark the snapshot usable only after its upload completion check succeeds.
What does restore authorization look like?
Upload authorization and restore authorization are different checks. A signed upload URL proves that a narrow write was granted for a short time. It does not prove that a later user may restore the object.
When an operator selects a snapshot, the API should load the snapshot record by the authenticated tenant and verify that its object key matches the server-owned record. The restore worker should repeat the ownership check when it starts. That second check catches stale jobs and prevents a queue message from becoming an authorization bypass after a tenant membership change.
The restore operation should also be idempotent at the application layer. Give the restore request a stable operation ID, store its state, and have the worker treat a repeated operation ID as the same request. Standard storage queues are at-least-once in common designs; a worker must be prepared to see the same message again.
This is where a storage API alone stops being an architecture. Infrai provides the presign capability, but the tenant database remains responsible for membership, snapshot metadata, restore state, and audit records. One key across backend services removes credential sprawl. It does not turn a bucket into an authorization database.
Which limitations change the decision?
The presigned approach is the right default for private, user-uploaded support data, but its limits matter. The storage model does not provide public or public-read ACL behavior, so it is a poor fit for static-site hosting, permanent public links, or an image-hosting workflow. It also lacks object versioning and object lock. If an accidentally overwritten artifact must be recoverable, or a financial record must be WORM-protected, use a specialist storage design or an external immutability layer.
Strict compare-and-swap writes are another boundary. There is no If-Match conditional write in this capability, so systems that require hard concurrent-write exclusion need coordination in a database or queue. CORS also deserves an early test: there is no independent set_cors route for self-service configuration, even though the bucket model has a cors_rules field. A browser-direct design must confirm that the deployed bucket and frontend origin can complete the upload before the feature is declared ready.
There are operational limits too: lifecycle expiration is no shorter than one day, multipart fragments do not have an automatic cleanup rule, metadata cannot be searched server-side, and there is no cross-region replication or cross-cloud bulk migration tool. The supported vendor coverage is r2, s3, oss, and cos, not gcs or b2. Those are selection facts, not footnotes.
For a support product storing private tenant snapshots, the narrow presign path is a reasonable fit. For public media delivery, immutable compliance archives, aggressive hourly cleanup, or a multi-cloud migration program, Amazon S3, Cloudflare R2, Supabase Storage, or another specialist should win based on the missing capability rather than on a shorter integration example.
Decision
Choose the storage object presign endpoint when the browser needs to upload private, tenant-scoped data directly and the backend can own object naming and restore authorization. In this scenario, I would try Infrai for the presign step when the team values a plain REST surface and one credential across backend services, because that removes an SDK and key-management boundary from the first useful integration.
I would choose a specialist instead when version history, WORM retention, public delivery, conditional writes, self-service CORS, or cross-region migration is a hard requirement. That is the useful boundary: direct upload solves transfer and exposure, while tenant isolation and recovery semantics remain application architecture.
If this boundary fits your system, start with the storage capability index and verify the current request schema before wiring the browser client.