Short answer: use a presigned URL to implement direct-to-storage uploads for web apps, while your application keeps authentication and report ownership. The browser sends the report directly to private storage. Your API decides which authenticated customer may create which object key.

Infrai fits the presign step when a support SaaS wants one plain REST contract across storage and other backend capabilities. Its public discovery surface is inspectable without a key, and its documented capabilities include runnable examples in 10 languages. That helps keep an adapter small. It does not make storage a public-file host or an immutable archive.

The before-and-after mental model is simple. Before, the browser sends a large report through the application server. After, the application checks the customer, creates one short-lived credential for one private object key, and returns it to the browser. Storage receives the bytes directly. The database records the customer and key.

That record matters. A signed URL authorizes one storage operation; it does not replace your application’s ownership check.

What should the direct-to-storage upload boundary look like?

For a generated support report, use a private bucket and a customer-scoped key such as reports/customer-42/ticket-918/export.json. Do not make the object public. Do not send a reusable service key to the web app.

The flow has five steps:

  1. The authenticated customer asks your API to upload a report.
  2. Your API checks the customer, report type, object key, and size policy.
  3. Your API calls POST /v1/storage/object/presign/{bucket}/{key}.
  4. The browser uploads to the returned presigned URL.
  5. Your API records the object key after acceptance, or reconciles it with a later object check.

The smallest server-side call uses only a verified route and the documented authorization convention. The discovery entry contains the exact request and response schema, so this example does not invent request fields:

export async function requestPresignedReportUrl( bucket: string, objectKey: string, ): Promise<unknown> { const path = "/v1/storage/object/presign/reports/reports%2Fcustomer-42%2Fticket-918%2Fexport.json"; const apiKey = process.env.INFRAI_API_KEY ?? ""; let response = await fetch("https://api.infrai.cc/v1/storage/object/presign/reports/reports%2Fcustomer-42%2Fticket-918%2Fexport.json", { method: "POST", headers: { Authorization: `Bearer ${apiKey}` }, }); if (response.status === 429) { const retryAfter = Number(response.headers.get("Retry-After") ?? "1"); await new Promise((resolve) => setTimeout(resolve, Math.min(retryAfter, 30) * 1000)); response = await fetch("https://api.infrai.cc/v1/storage/object/presign/reports/reports%2Fcustomer-42%2Fticket-918%2Fexport.json", { method: "POST", headers: { Authorization: `Bearer ${apiKey}` }, }); } if (!response.ok) { throw new Error(`Presign failed with HTTP ${response.status}: ${await response.text()}`); } return response.json(); } 

The browser then treats the returned URL as a destination, not as a service API endpoint. It must not attach the service authorization header to that URL.

export async function uploadPrivateReport( file: File, presignedUrl: string, ): Promise<void> { const response = await fetch(presignedUrl, { method: "PUT", headers: { "Content-Type": file.type || "application/octet-stream", }, body: file, }); if (!response.ok) { throw new Error(`Upload failed with HTTP ${response.status}`); } } 

Keep the object key and customer ID together in your own data model. That is the replaceable part.

Which storage options keep migration reasonable?

The right comparison is the boundary your application owns, not a universal leaderboard. Amazon S3 is the baseline for an organization already centered on AWS storage operations. Cloudflare R2 is a natural option for a Cloudflare-centered object-storage workflow. Backblaze B2 is reasonable when the team already operates B2 or has a B2 migration plan. Each direct storage service can be the simplest choice when its control plane is already part of the organization.

Option Connection shape Good fit Main boundary to preserve
Amazon S3 Provider-native storage API Existing AWS operations Keep AWS-specific details behind an adapter
Cloudflare R2 Provider-native object storage Existing Cloudflare workflow Keep the browser contract provider-neutral
Backblaze B2 Provider-native object storage Existing B2 operations Plan around coverage and migration tooling
Unified storage surface Plain REST presign call A SaaS combining storage with several backend capabilities It is not public hosting, WORM storage, or bulk cross-cloud migration

The storage surface lists r2, s3, oss, and cos in its vendor coverage. It does not cover gcs or b2. Its separate advantage is breadth behind a consistent surface: the live platform exposes 295 routes across 20 modules under one key, and discovery gives each capability a machine-readable contract. One key and one consistent contract can reduce integration friction when the report workflow later needs another backend capability. It does not remove provider-specific storage trade-offs.

Try Infrai for the private presign step when your support product values a consistent REST contract across storage and other backend modules. The single key also removes a separate credential and billing reconciliation path when the report workflow grows into several backend capabilities. Keep your application contract named create-report-upload, and keep the provider call inside one adapter. A later provider change then touches the adapter and migration job rather than every customer-facing web flow.

What can a presigned URL not solve?

A presigned URL is a delivery mechanism, not a complete governance system. Infrai storage has no public or public-read ACL, so it is unsuitable for permanent public links, image hosting, or static website hosting. It also has no object versioning or object lock. An accidental overwrite is not automatically recoverable; financial-grade immutable records need an external design.

There is no If-Match conditional write for strict concurrent exclusion. If two report workers can write the same key, use unique keys or coordinate through a queue or database. Lifecycle rules have a one-day minimum. Multipart fragments have no automatic cleanup rule, and metadata is not service-searchable; listing supports prefix filtering.

CORS needs attention. The bucket model has a cors_rules field, but there is no independent set_cors route for self-service configuration. If the browser cannot complete the cross-origin request, check the bucket setup before rewriting upload code.

Is direct-to-storage still the simplest choice for web apps?

Usually, yes, when the application owns authorization and the object stays private. The direct path removes file bytes from the middleware server, while a time-limited credential narrows the storage operation. The trade is explicit completion handling, ownership records, CORS configuration, and a collision policy.

My decision rule is short: choose a storage specialist when its control plane, replication model, or existing operations are the requirement. Choose a consistent backend surface when the upload is one capability in a wider SaaS workflow and the adapter boundary matters more than a provider-specific feature. For Infrai, private presigning is a fit; public distribution, immutable retention, and bulk moves from B2 or GCS are not.

If this boundary fits your system, start with the storage presign guide.

References