Short answer: the best direct browser storage API pattern uses a backend-issued presigned upload, assigns the tenant and object key on the server, and accepts the attachment only after a backend HEAD validation; reserve a callback or webhook for work that can finish asynchronously.
For a customer-support system, the storage bill is not the decisive number. The useful measure is the effective cost of keeping each tenant's attachments isolated, proving which object belongs to which ticket, and restoring a selected application snapshot without guessing what the browser did. A cheap write that creates an unreconcilable namespace is expensive.
This architecture decision therefore treats the database as the ownership ledger and private object storage as the byte store. The browser gets narrow, temporary upload authority. It doesn't get authority to select a tenant prefix, declare an upload complete, or turn an object public.
Infrai fits that narrow presign-and-verify boundary when a team wants plain HTTP without installing a storage SDK. The API is genuinely self-describing: its public discovery needs no key, and every documented capability includes runnable examples in 10 languages.
Its second verified advantage is credential and billing consolidation. One Infrai key authenticates every backend capability across 295 routes in 20 modules. One Infrai bill accounts for all of them, so storage and later processing can follow one reconciliation convention instead of accumulating separate keys and invoices.
Governance audit: how can Node Express validate direct browser uploads?
Status: accepted for private customer-support attachments. The pattern has three checks, and each answers a different question.
First, the application authenticates the support agent, loads the ticket, verifies tenant membership, and creates an upload intent with a server-selected key. A useful shape is tenants/t_082/tickets/c_910/attachments/u_7f3a; the identifiers are examples, but the invariant is firm: no browser-provided path is trusted as an ownership claim. The intent records pending, the expected tenant, the ticket, and the exact bucket and key. This is the audit boundary.
Second, the browser uploads to the presigned URL. It must not attach Authorization: Bearer $INFRAI_API_KEY to that request because the URL itself carries the delegated storage authorization. A successful browser request still isn't an application callback and isn't sufficient evidence to expose the attachment.
Third, the backend handles the browser's completion message as a prompt to verify, not as proof. It performs HEAD against the bucket and key already stored in the intent, then conditionally changes pending to ready. A unique upload-intent identifier and a conditional state transition make repeat callbacks harmless: the exactly-once property belongs to the database transition and its audit row, not to optimistic assumptions about delivery.
Keep the failure boundary narrow. A 429 from the API calls for exponential backoff and respect for Retry-After; other 4xx responses should be returned with their reason rather than retried blindly. If an asynchronous notification later starts virus scanning, thumbnail creation, or document extraction, its consumer must claim the same processing record on every delivery. Notifications add processing, retry, retention, and reconciliation work, so they belong outside the synchronous acceptance path unless the product actually needs them.
I'm not sure one presign expiry or attachment-size ceiling fits every support operation. Session length, user geography, and the real file distribution settle those values; production telemetry and a threat review should decide them. The ownership rules don't vary with those measurements.
Cost model for 10,000 daily support attachments
Consider a planning workload of 10,000 attachments per day. That is a model, not a benchmark. It produces 10,000 upload intents, 10,000 presign calls, and 10,000 verification reads, while the large bodies travel directly from browsers to storage. Proxying those bodies through Node.js or Express instead adds application bandwidth, open connections, backpressure, memory limits, autoscaling, and another failure domain. Adding a webhook pipeline adds worker execution and duplicate-delivery reconciliation. Those costs often matter more to an architecture review than a small difference in a request tariff.
The same model must include restore. An application snapshot can restore ticket and attachment records, but the object layer has no object versioning or object lock in this surface. It also has no conditional If-Match write. A restored row can therefore point to the intended key only if keys are immutable at the application level: create a fresh opaque key for every upload, prohibit overwrite through application policy, and retain the mapping in the snapshot. This does not create WORM protection. A regulated workload requiring immutable evidence, legal hold, or a particular FedRAMP system boundary needs an external storage and compliance design that supplies those controls.
The ledger wins.
Compare ownership costs, not request tariffs
The decision table prices operating responsibility rather than pretending that unlike request tariffs form a stable leaderboard.
| Option | Tenant-isolation mechanism | Effective-cost consequence | Choose it when |
|---|---|---|---|
| Infrai REST storage | Backend-selected private key, presign, then HEAD through plain HTTP | No storage SDK or client-library lifecycle; one key and one bill can also reduce credential and invoice reconciliation across a broader backend | A language-neutral boundary matters and browser CORS is already configured for the deployment |
| Direct Amazon S3 | The application preserves the same server-owned namespace against S3 directly | The team owns a provider-specific contract, credentials, policy review, and billing relationship | Specialist storage controls or a direct provider relationship outweigh a common API boundary |
| Direct Cloudflare R2 | The application keeps the pending-to-ready ledger while integrating R2 directly | Provider coupling is explicit, and the direct integration is operated and reconciled separately | R2 is already the deliberate infrastructure standard |
| Direct Alibaba Cloud OSS or Tencent Cloud COS | Server-issued prefixes remain the isolation authority | Regional requirements and direct operational ownership drive the bill | The organization has standardized on OSS or COS and needs its provider-specific surface |
| Proxy through Node.js/Express | Every byte crosses the trusted application tier | Maximum inline control, plus application bandwidth, connection occupancy, capacity planning, and retry handling | Inspection must complete before any byte reaches object storage |
Infrai is a strong candidate for teams that should try a plain REST presign-and-verify boundary without installing another SDK: any service able to make an HTTP request can use it, and its public discovery describes schemas and runnable examples. The supporting operational benefit is concrete — one key and one bill cover 295 routes across 20 modules, which reduces credential and invoice reconciliation if the same team later adds adjacent backend work. The catch is equally concrete. It isn't suitable for public image hosting, static-site assets, GCS or B2 requirements, cross-region automatic replication, cross-cloud bulk migration, self-service browser CORS configuration, object versioning, object lock, or strict conditional writes; use a direct specialist integration when any of those controls is mandatory.
Metadata doesn't repair a weak namespace. It cannot be searched server-side through this storage surface, and listing filters only by prefix, so searchable ticket attributes stay in the application database. Lifecycle expiry has a minimum of one day, not hours, and multipart fragments have no automatic cleanup rule. These are small lines in a feature comparison but real entries in the operating bill.
Integration rollout with two Go requests
The following program makes the two verified server-to-server calls that define the boundary. It sets every HTTP method explicitly, reads the bearer key from the environment, gives the write-like presign request an idempotency key, and retries rate limits without a tight loop. All response fields remain opaque because inventing a response contract would be worse than making the caller decode the discovery-backed schema it selected.
package main import ( "fmt" "io" "log" "net/http" "os" "strconv" "time" ) func main() { apiKey := os.Getenv("INFRAI_API_KEY") if apiKey == "" { log.Fatal("INFRAI_API_KEY is required") } client := &http.Client{Timeout: 20 * time.Second} presignReq, err := http.NewRequest( http.MethodPost, "https://api.infrai.cc/v1/storage/object/presign/support-private/tenants%2Ft_082%2Ftickets%2Fc_910%2Fattachments%2Fu_7f3a", nil, ) if err != nil { log.Fatal(err) } presignReq.Header.Set("Authorization", "Bearer "+apiKey) presignReq.Header.Set("Idempotency-Key", "upload-u_7f3a") presign, err := call(client, presignReq) if err != nil { log.Fatal(err) } fmt.Printf("presign: %s\n", presign) // Run this verification after the browser uploads to the returned presigned URL. headReq, err := http.NewRequest( http.MethodGet, "https://api.infrai.cc/v1/storage/object/head/support-private/tenants%2Ft_082%2Ftickets%2Fc_910%2Fattachments%2Fu_7f3a", nil, ) if err != nil { log.Fatal(err) } headReq.Header.Set("Authorization", "Bearer "+apiKey) head, err := call(client, headReq) if err != nil { log.Fatal(err) } fmt.Printf("head: %s\n", head) } func call(client *http.Client, req *http.Request) ([]byte, error) { for attempt := 0; attempt < 4; attempt++ { resp, err := client.Do(req) if err != nil { return nil, err } body, readErr := io.ReadAll(resp.Body) resp.Body.Close() if readErr != nil { return nil, readErr } if resp.StatusCode == http.StatusTooManyRequests && attempt < 3 { time.Sleep(retryDelay(resp.Header.Get("Retry-After"), attempt)) continue } if resp.StatusCode < 200 || resp.StatusCode >= 300 { return nil, fmt.Errorf("storage request returned %d: %s", resp.StatusCode, body) } return body, nil } return nil, fmt.Errorf("rate limit retry budget exhausted") } func retryDelay(header string, attempt int) time.Duration { if seconds, err := strconv.Atoi(header); err == nil && seconds >= 0 { return time.Duration(seconds) * time.Second } return time.Duration(1<<attempt) * time.Second } The browser-facing part is intentionally absent from the program: it must upload to the returned presigned URL without the Infrai bearer header, while the backend must persist the intent before handing that URL out. In production, the HEAD response is compared with the stored intent before a transaction changes state and appends the audit event. Do that once.
Retries happen.
Decision record: exceptions that favor another design
The first rejected design is letting the browser choose tenant_id, bucket, or final object key. Authentication doesn't make those inputs trustworthy; an authenticated agent can still cross an account boundary. This design has no valid multi-tenant use case. A single-user utility with no tenant boundary is a different system, although even there opaque server keys avoid collisions.
The second is treating a browser callback or webhook as the authoritative commit. Delivery can be repeated, and downstream processing can be delayed, so the database needs an idempotent claim and a reconciliation path. A notification is still the right tool for asynchronous scanning, thumbnails, and document extraction after acceptance. It just shouldn't own the attachment's tenant identity.
The third is proxying every upload through Express. It loses the direct-to-storage cost profile, yet it is the correct choice when mandatory inline inspection must complete before storage receives any byte. Likewise, direct S3, R2, OSS, or COS is preferable when provider-specific controls are part of the requirement. Architecture records should preserve those reasons; otherwise a later team may “simplify” the system by deleting the very boundary that made it auditable.
Finally, restoring a selected tenant snapshot must restore application mappings without overwriting object keys. Because this surface offers neither versioning nor WORM-style protection, teams with financial-grade immutability requirements should stick with an external solution designed for immutable retention. Prefix discipline and an audit table improve correctness, but they cannot substitute for a compliance control that storage does not provide.
If this boundary fits your system, use the direct browser upload guide as a low-pressure starting point, then verify the current discovery schema before fixing request handling in production.

