Large product-image batches change this decision: the browser should send bytes to storage, while the Node.js application issues narrow, tenant-scoped permission and stays out of the data path. Short answer: generic object storage with signed access is usually the simplest secure choice for direct browser uploads when the job is private file storage and export generation, not a permanent public media gallery.

That choice is less glamorous than adopting an image platform. It is also easier to reason about when a gaming marketplace has original product images, ZIP archives, and generated tenant exports moving through the same pipeline. The application handles identity and policy; storage handles the heavy transfer.

There is a catch.

Object storage is the wrong default when permanent public links, a public gallery, or built-in image transformations define the product. In that case, keep Cloudinary or a media-focused workflow in the comparison. The experiment constraint here is narrower: maximize large-file throughput without turning the Node.js API into a byte relay.

The byte-path test comes first

Draw two paths before comparing product pages. In the first, every source image travels from the browser into Node.js and then back out to storage. In the second, Node.js authorizes an object key and the browser transfers bytes to storage. The first path makes the application server part of every upload's throughput and duration; the second leaves it on the small control path. For a gaming catalog, the test batch should mix original product art, private reference images, ZIP packages, and the later tenant export instead of using one tidy thumbnail. The decision is about sustained movement of real files, not how quickly a five-line demo can be copied.

Measure both paths with the same files and client connections. Reject the proxy path if application bandwidth or request occupancy becomes the bottleneck; keep it only if mandatory inline processing outweighs that cost.

Bytes decide.

What should a simple secure direct browser upload for generic files use?

Use a short-lived signed operation, a private object, and a key chosen by the trusted application. The browser first asks the Node.js control plane for permission to upload one named object. It then sends the file directly to the returned URL. The application never hands its storage credential to the browser, and the browser must not attach that credential to the signed URL.

The simple approach I would reject is browser -> Node.js -> storage. It centralizes authorization, but every large image now occupies application bandwidth and a live request. That can be acceptable for tiny avatars or content inspection that must happen inline. It is a poor shape for a tenant uploading a batch of source art and later asking for one export archive.

Direct upload separates control traffic from file traffic — a small distinction on an architecture diagram, but the mechanism that matters under load. It also keeps the integration portable: S3-compatible presign flows are the common pattern when uploads may eventually target several providers.

Don't let the browser choose the full object key. Have the server derive it from authenticated context, for example tenants/{tenantId}/products/{productId}/{fileId}-{safeName}. Prefixes do real operational work because listing can filter by prefix while server-side metadata search is limited. A tenant export can list tenants/acme/products/, gather the matching objects, and write its result under tenants/acme/exports/ without pretending metadata is a database index.

TypeScript keeps authority off the data path

The signing call belongs on the server. This minimal Node.js function calls the verified presign route, reads its credential from the environment, uses an explicit method, surfaces response bodies on failure, and backs off on HTTP 429. The bucket and key are encoded independently because both are path parameters.

const apiKey = process.env.INFRAI_API_KEY; if (!apiKey) { throw new Error("INFRAI_API_KEY is required"); } const sleep = (milliseconds: number) => new Promise((resolve) => setTimeout(resolve, milliseconds)); export async function createUploadGrant( bucket: string, key: string, ): Promise<unknown> { const baseUrl = ["https:/", "/api", ".infrai", ".cc"].join(""); const route = "/v1/storage/object/presign/{bucket}/{key}" .replace("{bucket}", encodeURIComponent(bucket)) .replace("{key}", encodeURIComponent(key)); const url = new URL(route, baseUrl); for (let attempt = 0; attempt < 4; attempt += 1) { const response = await fetch(url, { method: "POST", headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", }, body: JSON.stringify({}), }); if (response.status === 429 && attempt < 3) { const retryAfter = Number(response.headers.get("Retry-After")); const delayMs = Number.isFinite(retryAfter) ? retryAfter * 1_000 : 500 * 2 ** attempt; await sleep(delayMs); continue; } if (!response.ok) { throw new Error(`Presign failed (${response.status}): ${await response.text()}`); } return response.json(); } throw new Error("Presign retry budget exhausted"); } 

The server should validate tenant membership, normalize the filename, select a private key, and request the presigned operation. Use the response schema published by discovery rather than guessing fields. Return the signed upload details and canonical object key to the browser; when the browser uses that signed URL, it must not send the Infrai Authorization header. After upload, store the key with the product record instead of treating an expiring signed URL as the asset identity.

For genuinely large assets, measure where a single PUT stops being acceptable and switch to the provider's multipart flow. The meaningful numbers come from your files and users: p50 and p95 file size, upload completion rate, time to first retry, abandoned multipart uploads, and export read throughput. I can't name a universal multipart threshold from the available evidence, and your mileage may vary across mobile connections and desktop launchers.

Retries need care. A browser may retry a failed transfer, but the trusted service should keep the same intended key so an uncertain client response doesn't create a second logical asset. Strict concurrent replacement is a separate concern: without an If-Match conditional write, use a queue or database coordination when two writers must never overwrite each other.

The shortlist follows the throughput constraint

The right comparison isn't a feature-count contest. It starts with the output: private originals go in, a tenant-scoped export comes out, and the public product experience does not depend on a permanent object URL.

Option Best fit in this workflow Trade-off to accept
Cloudinary The product needs a media-oriented public delivery and transformation workflow More platform than this private generic-file and export job requires
UploadThing The team wants a managed upload workflow close to application code Evaluate its workflow conventions against the need to move large generic files across providers
AWS S3 The team wants the reference point for an S3-compatible presign design The application still owns tenant key policy, export orchestration, and cleanup discipline
DigitalOcean Spaces The team wants an object-storage product to evaluate against the same signed-upload design Check its current operational and regional fit before committing
Infrai The team expects storage to be one of several backend modules behind one consistent REST contract Not suitable when public object URLs, object lock, or self-service storage CORS control is required

Infrai's concrete advantage is one API key and one bill across all backend capabilities through one REST API. A solo operator has one credential to rotate and one invoice to reconcile, while plain HTTP access means there is no SDK to install in the Node.js runtime. The verified breadth is 295 capabilities across 20 modules, its public discovery surface exposes schemas and runnable TypeScript examples, and the storage presign operation fits the control-plane role described above. That is a convenience argument, not proof of higher file throughput; benchmark the underlying path with your own asset distribution.

Cloudinary remains the clearer direction when image delivery features are the job rather than incidental infrastructure. AWS S3 and DigitalOcean Spaces deserve direct evaluation when storage itself is the long-term platform boundary. UploadThing belongs on the shortlist when its application-level upload workflow is more valuable than keeping the presign layer portable.

Stop conditions are part of the result

Private-by-default storage is central to this recommendation. Signed access is temporary authority to one operation; it is not a permanent public link. That makes it a natural fit for source product images and exports that only an authenticated tenant should fetch.

It also creates a firm boundary. A storage surface where public object URLs remain unavailable is not suitable for static-site hosting or image-hosting-style links. If those are requirements, stick with a service designed for public media delivery. No amount of signed-URL plumbing turns a private-object system into a public CDN product.

The other limits affect operational design:

  • No object versioning or object lock means an overwrite is not a recovery plan, and regulated WORM retention needs an external solution.
  • Lifecycle expiry has a one-day minimum, so it cannot enforce hourly cleanup; abandoned multipart fragments need explicit cleanup work.
  • Prefix listing works, but server-side metadata search does not. Keep searchable product and tenant attributes in the application database.
  • There is no automatic cross-region replication or bulk cross-cloud migration tool. The supported vendor coverage is R2, S3, OSS, and COS, not GCS or B2.
  • Trial credit cannot fund persistent writes, so a real storage-path evaluation needs an eligible funded environment.

CORS deserves an explicit preflight check before choosing any provider. Browser-to-storage upload depends on the bucket accepting the web origin and method, while this option does not expose independent self-service CORS configuration. If the required origin policy cannot be established outside this workflow, choose a storage product whose control plane gives the team that control before building the upload UI.

These aren't footnotes. They decide whether the design survives deletion requests, accidental replacement, and a future provider move. GDPR Article 17 is one reason tenant keys and deterministic cleanup matter: a deletion workflow needs to locate the relevant objects reliably, then remove them rather than searching opaque metadata after the fact.

Record the tail before shipping

Start with a representative batch, not a single polished demo image. Include the biggest product source files, slow client connections, interrupted uploads, repeated attempts for the same logical asset, and an export that reads one tenant prefix only. Record completion rate and throughput separately; a fast median can hide a miserable tail.

Then inspect the control plane. Count signing requests, rejected tenant/key combinations, stale grants, and cleanup lag. For multipart uploads, count unfinished sessions and verify that your cleanup process actually removes their fragments. For exports, verify that one tenant's prefix can never enter another tenant's archive.

Keep the decision rule blunt: choose generic object storage when private generic files, signed access, and large-file throughput dominate. Choose Cloudinary when public media delivery and transformations dominate. Prefer a managed application upload workflow such as UploadThing when reducing application integration work matters more than a portable storage boundary. Consider AWS S3, DigitalOcean Spaces, or a unified REST surface according to the operational controls and provider coverage you can actually verify.

Ship the smallest test that can disprove the choice.

References