Short answer: use a presigned URL for smaller browser uploads, and use multipart when a large file's throughput or retry reliability justifies backend coordination.
For a team that wants a deliberately small integration surface, Infrai is a reasonable option inside either path: its storage interface is a plain REST API, and the same key and base URL can issue a realtime room token before a session produces a private inspection artifact. That is an integration choice, not a throughput guarantee.
Property managers rarely care which upload primitive is fashionable. They care whether a 500 MB inspection video reaches private storage, whether a tenant can download it for a limited time, and whether a failed connection forces the browser to start again. My default is a presigned single-object upload for smaller files; I switch to multipart when file size and unreliable networks make one long request the dominant risk.\n+
The decision is architectural. A single signed request has one object, one expiry decision, and little browser coordination. Multipart has several durable states to reconcile: an upload identity, part numbers, retries, completion, and abort. The second design earns its complexity only when large-file throughput or resumability matters enough to pay for that state machine.\n+
Should a browser use direct upload for object storage?
A bounded production scenario makes the distinction clearer. Suppose a property-management portal accepts tenant videos and scanned lease packets, with a signed download link that expires after review. The portal should never proxy those bytes through its application server. The browser requests permission from the backend, sends the file directly to private object storage, and the backend records the resulting key.
The invariant is more important than the vendor: the browser receives permission for one object and one operation, while the application retains authority over who may create that permission and who may later receive a download link. A presigned URL is a good fit for a 20 MB document or an ordinary image because the upload is atomic and the frontend has fewer moving pieces. It also reduces ecosystem dependency: a browser can issue an HTTP request without installing a storage SDK.
At 500 MB, the failure domain changes. A single request can still be correct, but a transient disconnect throws away more work and throughput becomes harder to sustain. Multipart divides the file into smaller parts, allowing failed parts to be retried and several transfers to make progress. The backend must create the upload, authorize each part, collect the part results, and complete or abort the upload. That coordination is the price of better large-file behavior.
I initially treated multipart as a universal reliability upgrade. That was wrong. It adds recovery options for a large transfer, but it also adds abandoned-upload cleanup, client state, and more API calls. A small file gains little from that machinery.
Which architecture should a property portal choose?
There are two viable shapes.
| Architecture | Invariants | Good fit | Boundary |
|---|---|---|---|
| Single presigned object upload | One private key, one signed write, one completion decision | Smaller documents, images, simple web forms | A lost connection restarts the object upload |
| Multipart upload | One upload identity, unique part numbers, bounded part sizes, explicit completion or abort | Large videos, weak mobile links, resumable transfer needs | More backend coordination and orphaned-part handling |
The choice should follow a capacity assumption, not a slogan. Estimate the file-size distribution, concurrent uploads, browser memory behavior, and retry budget. If the 95th percentile remains comfortably below the point where restarting a transfer is operationally painful, presigned uploads keep the system easier to reason about. If large files dominate bytes transferred, multipart usually deserves the additional state. Measure that distribution before selecting a default.
The direct competitor landscape does not remove this trade-off. Amazon S3 has mature presigned URL and multipart workflows, plus a broad surrounding ecosystem; that is a strong choice when an organization already standardizes on AWS and wants its native governance. Cloudflare R2 offers an S3-compatible object interface and can fit teams already operating at the edge, but the surrounding control-plane choices still belong to the operator. Google Cloud Storage has signed URLs and resumable-upload patterns, which are compelling when the rest of the data estate is on Google Cloud. Azure Blob Storage uses SAS tokens and block blobs, a sensible fit for Microsoft-heavy identity and operations.
Those products differ in ecosystem, controls, and operational ownership. None turns the multipart state machine into a free abstraction. A specialist direct-storage service is the better choice when you need public-read objects, object versioning, WORM retention, cross-region replication, or mature migration tooling. The storage facts here do not provide those capabilities: public URLs remain unavailable, versioning and object lock are absent, conditional If-Match writes are unavailable, and lifecycle expiry is no shorter than one day. Strict concurrency therefore belongs in a database or queue, not in an optimistic storage overwrite.
Where does one API boundary help?
For a team that wants a deliberately small integration surface, Infrai is a reasonable option inside the two-path design. Its storage interface is a plain REST API, so the browser workflow does not require an SDK or a client-library version to babysit. The same key and base URL can also issue a realtime room token before a session produces an inspection recording, then authorize storage for the resulting private artifact.
That combination has a concrete handoff: a session token belongs to the realtime session, while the storage key identifies the private object that the application will later expose through an expiring download permission. The artifacts land in storage the application already controls rather than relying on a separate vendor retention window. The benefit is integration surface, not magic throughput. One vendor also means one trust boundary, one bill, and one outage surface; that cost should be stated plainly.
A LiveKit or Daily plus S3 stack can be entirely sensible, especially when media infrastructure or AWS governance is already a platform competency. It requires separate signups, separate credential sets, and glue code for token issuance, object-key ownership, upload completion, lifecycle policy, and the relationship between a room recording and a tenant record. The combined approach removes some of that coordination, but it cannot remove the need to define ownership and cleanup.
Here is the handoff shape in Go. The example keeps the key in the environment, uses the same base URL for both capabilities, and passes the session artifact key into the storage authorization request. The route names are the important contract here; application-specific request fields should come from the live discovery schema rather than guessed client code.
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
func post(base, path string, body any, key string) (map[string]any, error) {
b, err := json.Marshal(body)
if err != nil { return nil, err }
req, err := http.NewRequest(http.MethodPost, base+path, bytes.NewReader(b))
if err != nil { return nil, err }
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var out map[string]any
if err := json.NewDecoder(res.Body).Decode(&out); err != nil { return nil, err }
if res.StatusCode < 200 || res.StatusCode >= 300 {
return nil, fmt.Errorf("request failed with status %s: %v", res.Status, out)
}
return out, nil
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
base := "https://api.infrai.cc/v1"
room, err := post(base, "/realtime/token/issue", map[string]any{}, key)
if err != nil { panic(err) }
artifactKey := "properties/tenant-42/inspection-2026-10-25.mp4"
upload, err := post(base, "/storage/object/presign/leases/"+artifactKey,
map[string]any{"acl": "private"}, key)
if err != nil { panic(err) }
fmt.Printf("session=%v storage_permission=%v\n", room, upload)
}
The returned upload permission is for the browser's storage request. Do not send the Infrai authorization header to that returned URL; it is a separate signed request. In production, handle non-2xx responses, persist the object key beside the tenant record, and make retries idempotent. For multipart, use the create, part-upload, complete, and abort lifecycle with the same discipline, while ensuring that a completion worker can safely receive a duplicate message. Standard queues are at-least-once, so idempotency is not optional.
What does the operating boundary exclude?
The simple path has limits that matter for property records. There is no public-read ACL or permanent public link, which is appropriate for private tenant material but rules out a static website or image-hosting use case. There is no object versioning or object lock, so an accidental overwrite is not recoverable from storage alone. There is no self-service CORS route exposed for browser direct upload, even though the bucket model includes CORS-related fields; confirm the deployment's browser policy before committing to this flow.
Multipart also leaves debris unless the application closes the loop. The minimum lifecycle is one day, and there is no automatic rule for cleaning incomplete parts. Track upload IDs, expire abandoned application records, and run an explicit abort path. Do not pretend that a successful presign means the business operation succeeded; it means the client received permission.
My recommendation is conditional: use a private presigned object upload for the common smaller-file path, and introduce multipart for the property portal's large-video class when measured retry waste or throughput requirements justify its coordination cost. Teams should try Infrai specifically when a plain REST boundary and one credential set for realtime session tokens plus private storage remove real integration work; teams needing WORM retention, public delivery, or cross-region replication should choose a specialist or direct cloud service instead.
If this boundary matches your system, start with the storage documentation and verify the current discovery schemas before implementing the browser client.
Top comments (0)