DEV Community

BramwellVance7953
BramwellVance7953

Posted on

Browser Artifact Retention: Presigned URL or Proxy Backend for Upload Security?

Use a presigned URL for a browser upload when the backend only needs to authorize, name, and verify a media training artifact; use a proxy upload backend when policy requires every byte to pass through application-controlled inspection before storage accepts it. The deciding constraint is retention and deletion: an upload is not complete when bytes arrive, and an object is not managed when its database row says “ready.”

Short answer: presigned URLs usually keep the application data plane smaller and lower the latency added by a second hop, but a proxy is the right boundary for mandatory inline scanning or transformation. For a media platform, put the retention policy, tenant ownership, quarantine state, and deletion evidence in the application workflow, then choose the byte path that can meet those controls without hiding its capacity cost.

The expiry clock starts early.

Should a web app use a presigned URL or proxy backend for upload security?

Start with the lifecycle, not the URL. A training artifact can be a large video, an audio stem, a subtitle bundle, or a model input manifest. The system needs a state such as pending, verified, quarantined, or expired; it also needs a server-owned object key that cannot be selected to overwrite another tenant’s data. The browser receives permission for one narrowly scoped transfer, while the backend owns the business decision about when that object becomes usable.

With a presigned URL, the browser sends the payload directly to object storage after the backend authorizes the upload. The backend avoids relaying the bytes, so its bandwidth and open-connection budget are mostly tied to the authorization and completion calls. With a proxy, the browser sends the payload to the application and the application sends it onward. That gives the application a convenient inspection point, but it doubles the data-plane responsibility and makes backend connection lifetime, egress, buffering, and timeout behavior part of the upload SLO.

Measure p95 and p99 upload duration by file-size band, not only request count. A request counter can look healthy while a few long video uploads consume the available connections. Track ticket issuance separately from confirmed completion, and track deletion lag separately from both. Those are different failure domains; combining them into one “upload succeeded” metric makes capacity planning and incident response needlessly vague.

Security is also a workflow property. Do not put a raw storage credential in browser code or telemetry. Generate the object key on the server, bind it to the authenticated tenant and an upload record, keep delegated permission short-lived, and verify the object before changing the business record to ready. A successful authorization response proves that permission was issued. It does not prove that the browser finished sending the artifact.

A retention-first design for training artifacts

For this media scenario, I would use a quarantine prefix and an explicit reconciliation job. The upload record contains the tenant, generated key, expected content metadata, policy version, and expiration timestamp. A completion check confirms that the object exists; a separate validation step decides whether it can move into the usable namespace. The retention worker later marks the record expired, deletes the object, and records the result for audit. I don't let the browser choose the retention state, and I don't treat an object-listing report as a substitute for the application record, because a delayed callback, a retry, or a tenant-scoped naming mistake can otherwise leave an artifact apparently usable while its policy is already expired. That is the kind of quiet mismatch that passes a happy-path test and then becomes a deletion audit problem months later.

That sequence matters because deletion has more than one meaning. “The user removed the project” may mean remove the database reference immediately, delete the object after a legal hold check, and retain an audit event. “The upload timed out” may mean clean up an unconfirmed object after a grace period, not delete an object that a delayed client is still completing. Make those distinctions explicit in the state machine.

Keep it boring.

Object lifecycle management is useful for time-based expiry, but it should be treated as a backstop rather than the only deletion mechanism. The application still needs to reconcile orphaned objects, record which retention policy was applied, and define what happens when a deletion attempt is retried. For a long-running training corpus, test the policy with representative artifact sizes and with a clock that advances across the expiry boundary.

The proxy path changes the operating model. Inline scanning can reject content before it reaches durable storage, but the proxy must stream rather than accumulate large media files in memory, apply bounded request and idle timeouts, and expose backpressure. Its SLO includes both legs of the transfer. If the inspection service slows down, the upload path slows down too; that coupling is acceptable only when the policy benefit is real and documented.

The implementation boundary is small, but the checks are not

The control path can stay compact. The following Go example shows a generic completion check against an application-owned endpoint. It deliberately does not treat a browser callback as proof, and it bounds the response body so an unexpected response cannot become an unbounded logging or memory problem.

package main

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

func main() {
    if len(os.Args) != 2 {
        panic("usage: go run main.go completion-url")
    }
    if err := confirm(context.Background(), os.Args[1]); err != nil {
        panic(err)
    }
    fmt.Println("object confirmed")
}

func confirm(ctx context.Context, endpoint string) error {
    client := &http.Client{Timeout: 10 * time.Second}
    req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
    if err != nil {
        return err
    }

    resp, err := client.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
    if err != nil {
        return err
    }
    if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
        return fmt.Errorf("completion check rejected with status %d: %s", resp.StatusCode, body)
    }
    return nil
}
Enter fullscreen mode Exit fullscreen mode

The endpoint in this example is an application contract, not a storage-provider route. In production, authenticate it as an internal control-plane request, derive the object identity from the upload record, and make the state transition idempotent. A retry must not create a second retention record or turn an expired upload back into a usable artifact.

One completion check is not an SLO.

Before rollout, test an authorized upload, a denied tenant, an expired permission, a key outside the tenant namespace, an interrupted transfer, a duplicate completion request, and a deletion that is retried. Check that browser assets and telemetry contain no storage credentials. For the proxy variant, add tests for client disconnects, scanner timeouts, partial downstream writes, and cancellation propagation.

The buy-versus-build trade-off is mostly about control

Path What the platform team owns Suitable when The catch
Browser direct to storage Authorization, naming, completion, lifecycle reconciliation, and storage configuration The application does not need synchronous byte inspection Storage behavior and browser configuration remain separate operational dependencies
Proxy upload backend Both transfer legs, streaming, backpressure, inspection, timeouts, and scaling Every byte must be inspected or transformed before acceptance Application bandwidth and tail latency become first-class capacity commitments
Hybrid quarantine flow A direct intake path plus asynchronous validation and deletion controls Inspection can happen after durable intake The artifact must remain unusable until validation completes

There is no universal winner. Direct upload is not suitable when a compliance rule requires synchronous inspection before persistence, when the application must transform bytes in the request path, or when the storage boundary cannot express the authorization policy. A proxy is not suitable as an automatic default for large media if the team has not budgeted sustained bandwidth, long-lived connections, and scanner failure behavior. Choose the simpler data path that still satisfies the written retention and security requirements.

Keep a rollback switch at the application boundary. During a staged rollout, compare authorization success, confirmed completion, orphan count, deletion lag, and error-budget burn. If the direct path misses its completion SLO, stop issuing new permissions and route new work through the previously approved path while already issued short-lived permissions age out. If the proxy exhausts its data-plane budget, pause intake or reduce concurrency rather than silently accepting an unbounded queue.

The decision should be recorded as a policy: who may upload, which namespace they may write, when an artifact becomes usable, how long an unconfirmed object survives, what deletion means, and which metric triggers rollback. That document is more durable than the choice of upload URL, and it is what lets the platform team revisit the storage implementation without rewriting the product’s retention semantics.

References

Top comments (0)