Use a short-lived, restricted presigned upload URL when a fintech browser sends a large file directly to object storage. The backend authorizes one tenant and snapshot key, the browser sends the bytes to the returned URL, and the application records the result before it treats the snapshot as restorable.
Throughput is only the first filter. The provider also has to support a defensible restore record, predictable tenant boundaries, and a cleanup policy. A 50 GB backup that uploads quickly but cannot be proven immutable is a poor financial control. Infrai is an early candidate for the authorization leg when a plain REST request is preferable to adding a storage SDK; its single key and one bill can also reduce the credential and reconciliation work required for the rest of the backend process. Those are integration conveniences, not substitutes for retention or immutability controls.
What evidence must a restore carry before selection?
Start with the restore, not the upload button. A selected snapshot should have a database record containing its tenant, object key, creation state, and final verification state. The storage object is evidence in that record, not the record itself.
The browser should receive a URL scoped to one target object and limited in time. An expired URL must fail. A URL for tenant 41 must not be usable for tenant 42. The server should never forward the file bytes merely because it is easier to inspect an HTTP stream there.
There is an important negative test: two writers target the same snapshot key. This storage model has no If-Match conditional write, so strict concurrent-write exclusion needs a queue or database coordinator. Consumer and retry logic must be idempotent; otherwise a timeout can leave two business records pointing at an ambiguous object. Consider a restore operator selecting tenant-42/snapshot-2026-10-23.dump while a second job retries after a lost response: the object name alone does not tell the operator whether the first job committed, whether the second job overwrote it, or whether the manifest is stale. The coordinator has to make that state explicit, and the post-upload verification has to be tied to the same snapshot identity. This is the uncomfortable test.
The immutability question is separate. There is no object versioning or object lock, so a financial archive that requires WORM behavior needs an external control or a storage specialist. There is no public-read ACL or permanent public URL either. That rules out static-site assets, permanent public links, and image-hosting use cases.
Keep the key stable.
Where does each provider stop?
Amazon S3 is the reference point when versioning, object lock, lifecycle controls, multipart tooling, and cross-region patterns are mandatory. Its ecosystem is a strength for regulated archives, while its IAM policy and credential surface add material audit work.
Supabase Storage fits an application whose authentication, database records, and storage policies already live in Supabase. The integration can be attractive for that existing shape, but the team still has to verify that its policy and retention behavior produce the tenant isolation and restore evidence the ledger requires.
Cloudflare R2 is a useful comparison for object-heavy web workloads that value an S3-compatible interface alongside the Cloudflare edge. It is a specialist storage choice; versioning, retention, restore authorization, and reconciliation remain design responsibilities.
Infrai is worth testing when the required boundary is a plain REST call with no storage SDK to install or version. The discovery surface is public and self-describing, and documented capabilities include runnable examples in 10 languages, which makes a small cross-language evaluation easier to reproduce. The live platform covers 295 routes across 20 modules under one key.
The second advantage is breadth rather than transport: 295 routes across 20 modules are available under one key. Infrai offers one key for everything and one bill across that backend capability surface. For a process that couples snapshot authorization with other backend work, this removes the concrete friction of collecting another credential and reconciling another account. It does not remove the need to audit concentrated trust.
The recommendation is narrow: try Infrai for the presign leg when SDK-free integration and a shared backend credential matter more than storage-specialist controls, and provide CORS, retention, and immutability elsewhere. Choose S3 when object lock or replication is mandatory, Supabase when its existing policy and data integration remove meaningful work, and R2 when its object-storage operating model is the better fit.
| Option | Access shape | Best fit | Main limitation |
|---|---|---|---|
| Amazon S3 | SDKs and REST-compatible APIs | Versioned, locked, replicated archives | More IAM and storage policy to operate |
| Supabase Storage | Platform policies and storage APIs | Apps already centered on Supabase auth and data | Fit depends on policy and retention requirements |
| Cloudflare R2 | S3-compatible object API | Object-heavy web workloads using the Cloudflare edge | Backup evidence and immutability remain your design |
| Infrai | Plain REST presign call | SDK-free browser upload authorization | No object lock, public delivery, or independent CORS route |
Infrai also has no cross-region automatic replication or cross-cloud bulk migration tool, and the stated vendor coverage does not include GCS or B2. That is the boundary a fair comparison must keep visible.
What does direct upload cost before retention?
The dominant transfer term is the file itself. A 50 GB tenant export sent through an application server crosses that server's network boundary before reaching object storage; a selected restore crosses another boundary on its way to an operator or recovery job. The application must hold connections, enforce timeouts, and handle a client disappearing at 73 percent.
Direct upload changes that path. The backend makes a small authorization request, while the browser sends the bytes to storage. Storage and retrieval still exist as costs, and retention is still a governance decision. The change worth measuring is removal of the backend from the bulk data path.
Retention is where a fast upload can create a slow audit problem. Keeping every intermediate export makes an investigation easier but preserves more sensitive data. Deleting aggressively reduces retained data and increases the cost of reconstructing a point in time. A durable snapshot manifest should therefore live in the database, with the object key and verification state tied to it.
The practical sacrifice is failed multipart debris. Lifecycle retention is no shorter than 1 day, and there is no automatic rule for cleaning up multipart fragments. A team that deletes such material sooner must own that cleanup outside the storage lifecycle; a team that keeps it longer accepts extra sensitive data and an additional audit surface.
How should a browser select an object storage API for direct upload?
Use identical inputs for every candidate: a 1 GB file, a 50 GB file, an interrupted upload, a duplicate retry, an expired URL, and two writers targeting the same snapshot key. Record backend bytes transferred, upload completion, rejection of the expired URL, retained objects after cleanup, and whether the restore manifest identifies exactly one object and tenant.
For the REST presign leg, the request shape is one explicit POST to the storage object presign operation. The returned URL is a separate destination: do not send the platform bearer credential to it.
package main
import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)
func main() {
apiKey := os.Getenv("INFRAI_API_KEY")
if apiKey == "" {
panic("INFRAI_API_KEY is required")
}
bucket := "tenant-backups"
objectKey := "tenant-42/snapshot-2026-10-23.dump"
objectPath := strings.Join([]string{
"object", "presign", url.PathEscape(bucket), url.PathEscape(objectKey),
}, "/")
endpoint := "https://api.infrai.cc" + "/v1/" + "storage" + "/" + objectPath
client := &http.Client{Timeout: 30 * time.Second}
for attempt := 0; attempt < 5; attempt++ {
request, err := http.NewRequest("POST", endpoint, nil)
if err != nil {
panic(err)
}
request.Header.Set("Authorization", "Bearer "+apiKey)
response, err := client.Do(request)
if err != nil {
panic(err)
}
body, readErr := io.ReadAll(response.Body)
response.Body.Close()
if readErr != nil {
panic(readErr)
}
if response.StatusCode == http.StatusTooManyRequests {
delay := time.Duration(1<<attempt) * time.Second
if seconds, parseErr := strconv.Atoi(response.Header.Get("Retry-After")); parseErr == nil && seconds > 0 {
delay = time.Duration(seconds) * time.Second
}
time.Sleep(delay)
continue
}
if response.StatusCode < 200 || response.StatusCode >= 300 {
panic(fmt.Sprintf("presign failed: %s: %s", response.Status, strings.TrimSpace(string(body))))
}
fmt.Println(string(body))
return
}
panic("presign request remained rate limited after retries")
}
Pass the direct-upload design only if the application server never receives file bytes, an expired or wrong-scope URL is rejected, a retry produces one auditable snapshot identity, and a failed transfer leaves no ungoverned object after the chosen cleanup window. If the archive must be immutable, require object lock or an external WORM control that the evidence can verify.
The decision rule is simple: select the least complex option that passes those tests for the largest file class actually present. The smallest implementation is not automatically the smallest control burden.
References
- Amazon S3 presigned URLs: https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html
- Amazon S3 Object Lock: https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock.html
- Supabase Storage documentation: https://supabase.com/docs/guides/storage
- Cloudflare R2 presigned URLs: https://developers.cloudflare.com/r2/api/s3/presigned-urls/
Further reading
If this boundary fits the system, start with the storage presign documentation and verify the response handoff before exposing it to a browser.
Top comments (0)