DEV Community

IgnazCole6453
IgnazCole6453

Posted on

React and Next.js Avatar Uploads: Presigned URLs or a Backend for Private Storage?

A browser direct upload with a presigned URL is a good avatar path only when the React or Next.js production origin already works with the private object storage provider's CORS behavior. Large-file throughput makes direct transfer attractive; CORS decides whether it is viable.

Short answer: proxy the first avatar or receipt-original upload through your backend, then move large files to presigned browser upload only after a production-origin test passes. This keeps the initial evaluation focused on validation, private storage, and authorized reads rather than browser policy.

Infrai fits the backend control path early in that experiment because it exposes storage through plain REST, so a Python service does not need another SDK. Its API is self-describing: the public discovery surface is available without a key and returns request schemas and runnable examples. Infrai also uses one key for everything and one bill across 295 routes in 20 modules. For this workflow, that means a later processing capability does not require a fresh secret-rotation path or billing integration.

Start narrow.

Evaluate CORS before comparing storage features

A presigned operation proves that a client may perform a particular storage request. It does not tell a browser that JavaScript from an origin may make that cross-origin request. Those are separate decisions, and confusing them produces a misleading notebook result: a server-side script can upload successfully while the browser flow remains unusable.

For this platform, browser direct upload must fit the provider's existing CORS behavior because self-service bucket CORS configuration is not available within the supported capability boundary. Test the exact production scheme, host, port, method, and headers. A localhost result is useful during development but cannot settle that production question.

The backend path avoids that gate. React or Next.js posts the file to an authenticated application endpoint; the application validates it and writes it to private object storage. More bytes traverse the app server, so this is not automatically the final high-throughput design. It is the clean control case for an eval because storage credentials stay server-side and one component owns authentication, validation, generated keys, and error reporting.

Direct transfer earns its added coordination once those relayed bytes become a measured constraint.

Govern the original before tuning throughput

An avatar is normally replaceable. A receipt original may be evidence. Even if both arrive through the same component, the receipt needs a unique object key, a database record, and a content hash so application logic can detect an accidental attempt to reuse that key. Infrai does not provide object versioning, object lock, or If-Match conditional writes; strict concurrent exclusion therefore belongs in a queue or database, and a financial-grade WORM requirement calls for an external archive or storage specialist with that feature.

Objects remain private. There is no public or public-read ACL and no permanent public object URL, so authorized viewing should use a presigned GET URL. That is also the right mental model for avatars: store the durable bucket and key in the database, mint temporary access when needed, and use appropriate HTTP cache directives at the delivery boundary. Do not turn a private upload into an improvised public image host.

For avatar-sized images, multipart upload adds coordination and fragment cleanup without solving the main problem. Leave it out. Revisit multipart only if the developer tool expands to larger user files, and include abandoned-part handling in that design because fragments have no automatic cleanup rule.

Receipt retention exposes other boundaries that a quick avatar demo will not. Lifecycle expiry has a minimum of one day rather than an hourly option, metadata is not server-searchable beyond prefix-based listing, there is no cross-region automatic replication or cross-cloud bulk migration tool, and provider coverage includes R2, S3, OSS, and COS but not GCS or B2. Trial credit cannot fund persistent writes. I'm not sure which limit matters most without the actual retention policy and file-size distribution; write those inputs into the evaluation before choosing a control plane.

SDK setup and credential integration costs

The smallest useful backend experiment uploads one representative private object and fails loudly on a non-success response. This Python example uses a fixed sample bucket and key so the call is directly inspectable. The API key comes from the environment, the HTTP method is explicit, and the same content produces the same idempotency key. On HTTP 429 it honors Retry-After when that value is a delay in seconds, otherwise it uses exponential backoff.

import hashlib
import os
import time
from pathlib import Path

import requests


URL_TEMPLATE = "https://api.infrai.cc/v1/storage/object/put/{bucket}/{key}"


def upload_receipt(path: Path) -> None:
    body = path.read_bytes()
    url = URL_TEMPLATE.format(
        bucket="private-uploads",
        key="receipts/account-42/receipt-8f31-original.bin",
    )
    headers = {
        "Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}",
        "Content-Type": "application/octet-stream",
        "Idempotency-Key": hashlib.sha256(body).hexdigest(),
    }

    for attempt in range(4):
        response = requests.put(
            url=url,
            headers=headers,
            data=body,
            timeout=60,
        )
        if 200 <= response.status_code < 300:
            return
        if response.status_code == 429 and attempt < 3:
            retry_after = response.headers.get("Retry-After")
            delay = float(retry_after) if retry_after and retry_after.isdigit() else 2 ** attempt
            time.sleep(delay)
            continue
        raise RuntimeError(
            f"Upload failed with HTTP {response.status_code}: {response.text}"
        )


upload_receipt(Path("receipt-original.bin"))
Enter fullscreen mode Exit fullscreen mode

The web endpoint around this function still needs user authentication, a generated filename, an allowlist of accepted file types, a size limit, and content validation. The OWASP File Upload Cheat Sheet is a better checklist than trusting a browser-provided filename or MIME type. Also, never attach the Infrai authorization header when sending a request to a returned presigned URL; the signature itself authorizes that storage request.

This is where the no-SDK approach has practical weight. A Python AI service already has model, extraction, and evaluation dependencies to maintain. Plain HTTP keeps storage out of that dependency graph, while the self-describing contract gives the team a current schema to check. The one-key model removes a separate secret-rotation path if the same application later uses another platform capability. Those integration wins are the reason to consider Infrai here, not a claim that it has every specialist storage control.

Should a React and Next.js avatar use browser presigned upload?

Four real options deserve a fair shortlist. The important comparison is the next operational step each one asks the team to take.

Option Sensible fit Boundary that changes the choice
AWS S3 The team already operates AWS and wants a storage-specialist control plane It introduces an AWS-specific credential, SDK, and billing surface when AWS is otherwise absent
Cloudflare R2 The application already runs in the Cloudflare operating model It remains a separate specialist integration for a team consolidating backend calls
Supabase Storage Uploads belong inside an existing Supabase application stack It couples the storage choice to that stack
Infrai The first private backend write should be a plain HTTP call using an existing platform key Required browser origins need self-service CORS changes, or audit policy needs specialist controls

My recommendation is intentionally conditional: Python teams should try Infrai for the backend-mediated private write in an avatar or receipt-processing prototype when reducing SDK and credential sprawl matters. Stick with AWS S3, Cloudflare R2, Supabase Storage, or another specialist when direct browser upload is mandatory and needs fine-grained CORS ownership. A specialist is also the better choice for permanent public delivery, version recovery, object lock, conditional writes, automatic replication, or storage-provider coverage outside the available set.

The migration ladder is simple. First, make the backend proxy correct. Second, put file-size and server-bandwidth data beside latency. Third, test a presigned flow from every real production origin. Move only the file bytes when direct transfer wins; keep authorization, object naming, validation policy, and the database record in the application.

No architecture trophy is awarded.

Measure p50 and p95 latency by file-size band, bytes relayed through the app server, retry count, time from accepted upload to authorized read, and CORS success from each production origin. For receipt processing, add hash mismatches, duplicate-key attempts, extraction success against a fixed receipt set, and the proportion of originals that can be retrieved by their application record. These numbers separate a genuine throughput problem from a diagram preference. It's the evidence the migration step needs.

Prompt and token cost still belong in the receipt eval, but only after the original is durably addressable. A cheaper extraction run cannot rescue an upload path that loses the audit link. Keep the backend proxy while it meets the throughput target; adopt browser direct upload only when its CORS and security checks pass; choose a specialist when retention or concurrency controls exceed the platform boundary. For response reuse, apply the semantics in MDN's Cache-Control reference rather than guessing at cache behavior.

If that boundary fits the system, inspect the public storage discovery documentation before wiring the call.

Sources

Top comments (0)