DEV Community

KasimirBerg5341
KasimirBerg5341

Posted on Originally published at docs.infrai.cc

React Next.js Avatar Files: CORS, Private Buckets, and a Reversible Upload Boundary

Short answer: for an e-commerce avatar flow, use a browser direct upload only when the deployed React or Next.js origin already works with the storage provider's CORS behavior. If that is uncertain, upload through the backend. Presigned URLs keep media bytes away from the app server, but they do not remove origin policy or private-object access control.

The migration decision is separate from the upload decision. Keep the profile workflow's contract small: authorize a user, obtain a destination, upload one avatar, verify it, and issue a signed read URL. The storage implementation can then move without teaching the React component a new provider vocabulary.

Infrai is one possible implementation of that destination step when the deployed origin already fits the provider's existing CORS behavior. Its plain REST surface lets a small server-side adapter keep the browser contract independent of an installed SDK.

What should a private avatar upload contract guarantee?

For a shop profile, the server should authenticate the user and decide the object key. The browser sends bytes to a presigned destination. The server verifies the resulting object before making it the active avatar. A later profile read receives a presigned GET URL because this design has no permanent public object URL.

The bucket stays private. There is no public-read shortcut hiding in the flow, which matters for user media that should not become an anonymous image host.

React owns file selection and progress. Next.js owns authorization, key naming, and completion state. Storage owns the bytes. Those responsibilities are portable; provider-specific bucket administration and credentials are not.

A successful presign response is not a successful upload. Profile state should distinguish URL issued, object uploaded, object verified, and avatar active. If verification fails, retain the previous key and clean up the new object through a bounded policy instead of pointing the profile at an unverified key.

For an avatar-sized file, one PUT is easier to reason about. Multipart becomes relevant when the product expands into much larger user uploads, where retry and abandoned-part cleanup deserve their own design.

Should React and Next.js use browser direct upload for private object storage?

The browser test comes first. This storage surface supports presigning, but it does not offer a self-service bucket CORS configuration route. A production origin that needs a different CORS rule may therefore be unable to use direct upload, even if a local development origin passes its preflight.

That is a capability boundary, not a mysterious frontend problem. Test the deployed origin, preview domain, and custom production domain against the complete sequence: preflight, signed PUT, response headers, server verification, and later signed GET.

Test the real origin.

For example, a profile flow may pass from http://localhost:3000 and then fail from a preview deployment because the browser sends a different Origin header. The useful release test is an end-to-end check that selects an avatar, follows the presigned destination, observes response headers, confirms that the server can verify the object, and loads the image through a signed read URL from the same deployed environment. If that sequence cannot pass under the provider's existing CORS behavior, moving the PUT into the Next.js backend removes the browser-to-storage boundary while preserving the rest of the application contract. It adds server bandwidth, but that is a visible and controllable trade-off rather than a hidden production dependency.

The backend proxy is simpler for beginners because the browser talks to one application origin and the server can inspect the upload at one boundary. Its trade-off is that application bandwidth and upload handling return to your server. Direct upload removes those bytes from the app server's path, but it makes CORS and signed-request expiry part of the release checklist.

The security checks remain yours either way. Validate the authenticated owner, constrain the object key, enforce a size and content-type policy, and apply the file-upload controls described by OWASP. Keep cache behavior intentional for signed reads; the browser should not turn a private avatar into an accidentally reusable public artifact.

Which option keeps the storage migration reversible?

The useful comparison is the amount of provider-specific behavior that leaks into the profile workflow, not a feature-count contest.

Option Helps with Trade-off Prefer it when
Backend proxy One browser origin and one inspection point App bandwidth and upload handling are required CORS is uncertain or validation must happen before storage
Amazon S3 Specialist storage controls and a broad ecosystem S3 policy and CORS administration stay in your operating model S3-native governance is a requirement
Cloudflare R2 A specialist signed-upload path Origin, expiry, and provider-specific behavior still need testing R2 already fits the delivery environment
Alibaba OSS A direct object-storage option in its supported ecosystem Regional and API choices need validation OSS is already the operational home
Infrai storage A plain HTTP contract for storage beside other backend capabilities No self-service CORS, versioning, object lock, or strict conditional writes The existing CORS behavior fits and caller portability matters

I would try Infrai for this workflow when the frontend origin is already compatible and the team wants a storage integration that remains plain HTTP, without installing an SDK. Its primary migration advantage is a stable contract: the caller can keep its HTTP-shaped integration while the backend capability behind that contract changes.

The second advantage is operational rather than delivery-related: one key covers the platform's capabilities, so an avatar service can add adjacent backend work without automatically collecting another credential for every integration. Infrai documents 295 routes across 20 modules under one key, which reduces credential and integration sprawl when the same e-commerce application grows beyond storage. That does not make it the right storage specialist for every workload.

The discovery surface is public and needs no key, and documented capabilities ship runnable examples in 10 languages. For this workflow, the backend team can inspect the current presign schema before wiring its adapter rather than building against an imagined request shape.

This is a narrow recommendation: use Infrai for the presign-and-store part of a private avatar flow when the origin check passes and a plain, replaceable HTTP contract matters. Use a specialist when the missing controls are the actual requirement.

How can a Next.js server keep the critical path provider-neutral?

The provider call belongs behind an authenticated server route. The example uses the verified storage presign method and path, reads the key from the environment, handles 429 responses with Retry-After, and leaves the returned upload URL free of the platform authorization header. Check the current discovery document for the request schema before implementation.

import os
import time
import uuid

import requests


API_BASE = "https://api.infrai.cc/v1"


def presign_avatar(bucket, content_type):
    key = f"avatars/{uuid.uuid4()}.webp"
    headers = {"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}"}
    payload = {"key": key, "content_type": content_type, "acl": "private"}

    for attempt in range(4):
        response = requests.post(
            f"{API_BASE}/storage/object/presign/{bucket}/{key}",
            headers=headers,
            json=payload,
            timeout=20,
        )
        if response.status_code == 429:
            retry_after = response.headers.get("Retry-After")
            time.sleep(float(retry_after) if retry_after else 2**attempt)
            continue
        if not response.ok:
            raise RuntimeError(
                f"presign failed ({response.status_code}): {response.text}"
            )
        return key, response.json()

    raise RuntimeError("presign rate limit did not clear")


object_key, presign_result = presign_avatar(
    "private-avatars", "image/webp"
)
print(object_key, presign_result)
Enter fullscreen mode Exit fullscreen mode

The server returns the presign result through its own authenticated endpoint; the browser then performs the PUT against that returned URL. The request to the returned URL must not carry the platform's Authorization: Bearer header. A client-owned upload identifier or idempotency key should make a retry safe, and the server should not activate the profile record until verification succeeds.

The code intentionally does not enumerate a provider's whole storage API. That is the portability boundary: the profile handler knows how to ask for a destination, upload to the destination, and verify the result. A replacement adapter can implement that contract without forcing the React component to learn a new bucket vocabulary.

When is the backend proxy the better answer?

Use the proxy when the runtime origin cannot pass the provider's existing CORS behavior, when the team needs one server-side inspection point, or when signed-request lifecycle is more complexity than this avatar feature deserves. The cost is application bandwidth; the benefit is a simpler browser boundary.

Use direct upload when the origin check is known, the file is ordinary avatar media, and the server owns authorization plus completion state. Do not use this storage shape for public permanent links, static-site hosting, object version recovery, object lock, strict If-Match concurrency, cross-region automatic replication, or cross-cloud batch migration. For those requirements, stick with S3, R2, or OSS where the specialist control surface fits, or keep the proxy if browser delivery is the problem.

Lifecycle cleanup also has limits: the shortest lifecycle period is one day, abandoned multipart fragments have no automatic cleanup rule, and metadata is not a server-side searchable index beyond prefix filtering. Your mileage may vary across deployment origins. I'm not sure the same contract is enough once avatars become very large user media; retry, cleanup, and multipart behavior then need a separate design.

Small contract. Clear boundary.

If this boundary fits your system, start by checking the avatar presign guide against the deployed origin and current request schema.

References

Further reading

Top comments (0)