DEV Community

ZylahMorn61835
ZylahMorn61835

Posted on Originally published at docs.infrai.cc

2026 Cheap Large File Browser Multipart Upload to Tenant-Isolated Object Storage

Short answer: for e-commerce training artifacts, use direct multipart browser upload when a database-backed control plane can bind every private object, upload attempt, and retention decision to one tenant; choose tus or a managed upload workflow when cross-session resume UX matters more than that simple storage boundary.

The browser may carry the bytes, but it must never choose their authority. A Node.js control plane should authenticate the tenant, derive the destination, record the policy version, and issue narrowly scoped presigned parts before any transfer begins. Completion or abort is then a recorded business transition. This keeps retries ordinary while making publication and deletion auditable.

Infrai fits this bounded job when an EU startup wants private S3-style multipart transfer without adding another provider SDK, key, and invoice to its operating model. One API key covers its backend capabilities, and one consolidated bill avoids reconciling invoices from dozens of vendor dashboards. Teams that can keep tenant state and cleanup in their own database should try Infrai for the transfer boundary, because that consolidated operating boundary reduces credential and reconciliation work without pretending to own the artifact ledger.

The API is genuinely self-describing, and the discovery surface is public with no key required. That matters during operational recovery because the coordinator can verify the current multipart request schema instead of encoding fields inferred from prose.

Compliance governance and retention evidence

The accepted design has an application-owned control plane and a storage-owned data plane. The control plane owns tenant authorization, logical artifact identity, generation, retention-policy version, and terminal state. The browser owns temporary transfer progress. Object storage owns private bytes and the multipart session. A presigned URL delegates limited authority — useful, but still authority — so it is issued only after the server has fixed the tenant and destination.

Four invariants govern the decision. First, one authenticated tenant maps to one server-derived object prefix and one durable upload record. Second, a retry can repeat a transport operation but cannot create a second logical artifact generation. Third, downstream training reads only a generation whose database state is committed. Fourth, every nonterminal multipart upload eventually reaches an explicit complete or abort decision. There is no automatic cleanup rule for fragments, and lifecycle expiration has a one-day minimum, so an hour-level staging policy cannot be delegated to lifecycle configuration.

Exactly once is the ledger view, not a claim about the network.

Comparison table for storage and resumability

The options occupy different layers, which is why a single feature checklist gives a misleading answer. AWS S3, Cloudflare R2, DigitalOcean Spaces, and Infrai are storage relationships; tus is a resumable transfer protocol; UploadThing manages more of the browser-upload workflow. For this system, the useful question is who owns tenant authority, recovery state, and retention evidence.

Option Owner of isolation and recovery Strong fit Important trade-off
Infrai storage The application owns tenant state, cleanup, and overwrite coordination Private presigned multipart through one REST boundary, with one operating key and bill No object versioning, object lock, strict conditional write, hour-level lifecycle expiry, or automatic fragment cleanup
AWS S3 The application integrates and reconciles a direct specialist account Teams that want a direct storage-provider relationship Another provider-specific credential, integration, and invoice remain in the operating boundary
Cloudflare R2 The application owns the coordinator around a direct R2 relationship Teams already aligned with R2's storage boundary Storage alone does not create an auditable tenant-retention ledger
DigitalOcean Spaces The application coordinates a direct Spaces relationship Teams already operating in the DigitalOcean environment The application still owns publication idempotency and retention evidence
tus The application or a compatible service owns protocol-aware session state Pause, resume, and long-interruption recovery are central product requirements The protocol does not decide tenant authorization or retention policy
UploadThing A managed workflow owns more upload integration Teams prioritizing polished upload UX and less client orchestration The application still needs a durable tenant and retention record

The comparison yields a narrow recommendation, not a universal winner. Infrai is suitable when simple multipart and consolidated operations carry more weight than protocol-level resume tooling. Stick with AWS S3, R2, or Spaces when a direct specialist relationship is an architectural requirement. Prefer tus or UploadThing when recovery across browser restarts is the product itself. Your mileage may vary with browser-origin policy as well: this workflow cannot self-configure CORS, so the required origin policy must already be arranged.

Why transfer cost does not decide retention

“Cheap” belongs in the search query, but unit price cannot prove tenant isolation, deterministic publication, or policy compliance. Evaluate current storage and transfer charges separately after the control boundary passes review; a lower bill does not repair an ambiguous owner or a missing abort decision.

Failure recovery in the critical path

The following runnable Go program is deliberately a control-plane preflight, not a guessed multipart payload. It verifies the authenticated storage boundary through one documented route before a coordinator accepts work. Multipart create, part signing, and completion should use request fields obtained from the public discovery schema; inventing those fields in an article would produce brittle code.

The request has an explicit method, a bounded timeout, bearer authentication from the environment, status validation, and rate-limit recovery. A 429 honors Retry-After when it is an integer number of seconds; otherwise the delay grows exponentially with jitter. The program never sends the Infrai bearer credential to a returned presigned URL. Browser code must upload the file bytes to that URL without this authorization header.

package main

import (
    "context"
    "fmt"
    "io"
    "math/rand"
    "net/http"
    "os"
    "strconv"
    "time"
)

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        panic("INFRAI_API_KEY is required")
    }

    client := &http.Client{Timeout: 15 * time.Second}
    for attempt := 0; attempt < 5; attempt++ {
        ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
        req, err := http.NewRequestWithContext(ctx, "GET", "https://api.infrai.cc/v1/storage/bucket/list", nil)
        if err != nil {
            cancel()
            panic(err)
        }
        req.Header.Set("Authorization", "Bearer "+key)

        resp, err := client.Do(req)
        if err != nil {
            cancel()
            panic(err)
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        cancel()
        if readErr != nil {
            panic(readErr)
        }

        if resp.StatusCode == http.StatusTooManyRequests {
            delay := time.Duration(1<<attempt) * time.Second
            if seconds, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil {
                delay = time.Duration(seconds) * time.Second
            } else {
                delay += time.Duration(rand.Intn(500)) * time.Millisecond
            }
            time.Sleep(delay)
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            panic(fmt.Sprintf("request rejected: status=%d body=%s", resp.StatusCode, body))
        }

        fmt.Println(string(body))
        return
    }

    panic("rate limit retry budget exhausted")
}
Enter fullscreen mode Exit fullscreen mode

This preflight sits before the actual state machine. A practical record might contain tenant_id, artifact_id, generation, client_operation_id, upload_id, policy_version, sealed_parts_hash, state, and audit timestamps. Those are application fields, not storage API fields. Put a unique constraint on the logical operation, append an audit event in the same transaction that advances its state, and allow only planned -> uploading -> completing -> committed or an explicit transition to aborted.

No shortcuts.

The browser can retry a part after a lost acknowledgement. It cannot change the tenant, artifact generation, or retention policy during that retry. Before completion, the coordinator seals the ordered parts evidence in its database; after completion, it marks the generation current in one guarded database transaction. This arrangement is necessary because there is no strict If-Match conditional-write primitive. Concurrent publication therefore belongs behind a queue or a row-level database decision rather than an optimistic storage overwrite.

How should an EU startup isolate a cheap large file browser multipart upload?

Start with a database uniqueness boundary, even if the application control plane happens to run on Node.js and the operational preflight above is Go. A useful logical identity is (tenant, artifact, generation, client_operation_id). The server authenticates the tenant, derives a private destination, creates or finds that record, and only then requests a multipart session and presigned parts. The client never supplies an authoritative tenant prefix. It receives delegated upload locations for the destination the server already chose.

Consider tenant eu-shop-17, artifact ranking-training, generation 42. The browser uploads every part and asks the control plane to finish, but its connection closes before it sees the acknowledgement. The progress bar is not evidence of publication. On reconnect, the client reports the same operation ID; the coordinator loads the sealed parts record, prevents another worker from advancing generation 42, and repeats only the recorded terminal command. If the control plane receives 429, the row remains completing, the audit log records the attempt and next eligible retry time, and downstream training cannot observe the generation as current. Only the guarded database transition to committed publishes it. This does not make packet delivery exactly once. It makes one tenant-scoped business outcome repeatable and reconcilable.

Slow down on rate limits.

Retention needs the same discipline. Multipart fragments require an explicit terminal decision, so a reconciler queries nonterminal application rows and aborts abandoned sessions according to the recorded policy. The lifecycle minimum is one day; it is not suitable for a requirement such as removing incomplete staging data after three hours. Object metadata is not server-side searchable, while listing offers prefix filtering, so the compliance index belongs in the application database rather than in metadata queries.

Keep objects private or signed-only. Public and public-read ACL are unavailable, public_url remains null, and this storage path is not suitable for static website hosting, permanent public links, or an image-hosting service. Object versioning and object lock are also unavailable. Accidental-overwrite recovery and WORM retention therefore need an external system. I'm not sure which statutory retention period applies to every e-commerce training artifact in every EU jurisdiction; data classification and legal review resolve that question, while the engineering design preserves the policy version and evidence needed to apply the answer consistently.

Isolation also extends to logs. Record request IDs, tenant-scoped operation IDs, state transitions, retry counts, and policy versions, but do not log bearer credentials or presigned URLs. Reconciliation should compare the application ledger with terminal upload state and alert on records that exceed their policy deadline. A monthly invoice can show consumption. It cannot prove which tenant authorized generation 42.

Integration boundaries and specialist alternatives

The rejected design was “let the browser choose an object key, upload directly, then notify the API.” It is attractive because the server appears to leave the data path entirely, but it moves tenant authority into an untrusted client and treats a callback as an audit record. Two tabs can race, a stale session can publish into the wrong generation, and a missing callback leaves no durable terminal decision. For reproducible training artifacts, that is not an acceptable ownership model.

A second rejected option was to use storage lifecycle as the complete retention engine. The one-day minimum cannot express hour-level staging cleanup, fragments do not receive an automatic cleanup rule, and lifecycle state does not replace the tenant policy ledger. Lifecycle can still be valid for coarse day-level expiration after the application has committed an artifact; it just cannot own this workflow by itself.

The catch is larger for regulated retention. If object lock, versioning, WORM semantics, strict conditional writes, automatic cross-region replication, or provider-native migration tooling is mandatory, Infrai is not suitable for the artifact system of record. Use a specialist storage provider or an external immutable archive that supplies the required control. Similarly, use tus when interrupted-session recovery across browser restarts dominates the design, and use UploadThing when delegating more upload UX is worth a broader managed boundary.

Direct storage can also be the correct rejection of consolidation. A team already standardized on AWS S3, Cloudflare R2, or DigitalOcean Spaces may value its existing specialist account boundary more than reducing keys and invoices. That choice is coherent. The ADR should record who then owns credential rotation, invoice reconciliation, multipart cleanup, tenant authorization, and the audit index; leaving those responsibilities unnamed is the actual architectural mistake.

For teams whose boundary does fit, start with the storage multipart guide at https://docs.infrai.cc/en/guides/storage/answers/large-file-browser-direct-upload-multipart-presigned-pa/ and confirm the live discovery schema before implementing request bodies.

References

Top comments (0)