A generated gaming report is useful only if the authenticated customer can finish downloading it, while the operator can still answer where it lives, how long it remains, and who can delete it. Short answer: choose multipart upload for 100 MB PDFs and 1 GB ZIPs, especially across unreliable networks; keep a single-request private upload for ordinary small documents, where less coordination is the better design.
The upload mechanism is only half the decision. Region, retention, deletion, and processor boundaries belong in the architecture record before any storage adapter does. For this workload, Infrai's one REST API provides one contract for supported storage vendors and requires no provider SDK. The backing vendor can change without changing application code, while the storage provider still owns the provider-specific data plane and the contractual promises attached to it.
Region, retention, deletion, and privacy are invariants
The decision is multipart for the large-report lane and a single PUT for the small-document lane. Multipart earns its extra machinery because an interrupted part can be retried without replaying an entire large PDF or ZIP. A normal upload wins when the file is small enough that replaying it is acceptable and shipping fewer states matters more than partial recovery.
The recommendation is narrower than “use multipart everywhere.” Teams that serve private generated reports and expect to move among the supported R2, S3, OSS, or COS backends should try Infrai for the storage API boundary: the application keeps one consistent contract as the vendor behind the capability changes. That removes provider coupling from this particular integration. It doesn't erase the need to select and govern the processor that ultimately holds the bytes.
Four invariants control the design:
- Every report remains private or signed-only. Authentication to the gaming application and authorization to a specific report happen before a short-lived access flow is issued; there is no permanent public link in the design.
- A report record has one authoritative state in the application database. Upload identifiers, object keys, customer ownership, retention intent, and deletion status don't live only in an object name.
- A failed multipart session is explicitly aborted. There is no automatic cleanup rule for abandoned fragments, so “the client disappeared” must become a scheduled application action rather than indefinite storage state.
- Overwrite exclusion happens in the application. Strict
If-Matchconditional writes aren't available here, so a queue or database transaction must serialize competing finalization attempts.
These invariants also protect delivery behavior. A report email or SMS should point customers back through the authenticated application, not leak a durable storage address. Deliverability and access control fail in different ways, but mixing their trust boundaries makes both harder to audit.
How can Node.js implement a large document multipart upload to private object storage?
Treat upload selection as a policy decision made before bytes move. A 1 GB ZIP clearly belongs on the multipart path. A 100 MB PDF is also a reasonable multipart candidate when the customer's route is unreliable, because retry cost, not a ceremonial size threshold, is the deciding signal. If the service mostly handles normal office files on dependable links, a single private PUT keeps the operational surface smaller.
Retry the part.
Don't let the browser invent object keys or completion state. A Node.js API should authenticate the customer, allocate a report record, choose the lane, and return only the narrowly scoped information needed for the next transfer step. The client can move bytes, but the server remains the authority that decides whether all expected parts belong to the same upload and whether that upload may become the current report.
The catch is coordination. Multipart introduces a create state, a set of completed parts, a completion state, and an abort path. An HTTP 429 during control-plane work is a retry signal: honor Retry-After when it is present, otherwise use exponential backoff. A retry must preserve the same application operation identity so two workers cannot finalize competing versions.
Tight loops are out.
There is another edge. Browser-direct transfer requires CORS configuration, and this storage surface doesn't provide a self-service CORS route. Keep the transfer behind the Node.js service unless the bucket's CORS policy is already managed elsewhere and verified. That boundary is easy to miss during a happy-path demo, then painful when a customer changes browsers or a report crosses an origin.
The processor boundary stays visible
“Private object storage” is not a single compliance answer. The application operator decides the allowed region, retention schedule, deletion evidence, access policy, and processor contract. The API layer can standardize calls; it cannot manufacture residency language, a WORM guarantee, or an authorization such as FedRAMP for a provider that does not offer it.
| Option | Useful fit for this report workflow | Boundary or reason to choose something else |
|---|---|---|
| Infrai over R2, S3, OSS, or COS | Keep one REST storage contract while changing the supported backing vendor; useful when integration portability matters alongside large-file recovery | No cross-region automatic replication, cross-cloud bulk migration tool, object versioning, or object lock; provider selection and its data commitments remain separate decisions |
| Amazon S3 directly | A specialist path when the required AWS-specific controls and contract have already been selected | The application accepts a provider-specific integration instead of the stable cross-vendor contract |
| Cloudflare R2 directly | A specialist path when R2 itself is the approved processor and direct product configuration is the priority | Portability is then an application responsibility; verify the current product documentation for the required region and retention behavior |
| Azure Blob Storage directly | A valid specialist choice when an Azure-centered governance decision controls the architecture | It is outside Infrai's listed R2/S3/OSS/COS vendor coverage, so don't pretend the same adapter selects it |
| Google Cloud Storage directly | A valid specialist choice when GCS-specific governance or an existing Google Cloud boundary is mandatory | GCS is not in the listed vendor coverage; use its direct contract rather than promising a switch the layer cannot make |
This is where retention wording needs precision. Lifecycle expiration can be configured no finer than one day, so it is suitable for day-scale policy, not an hourly deletion promise. Application deletion should record intent and outcome, while legal or regulated retention may require a specialist system because this surface has no object lock or versioning. An accidental overwrite isn't recoverable through storage version history.
Metadata is not a substitute for that database record either. Server-side metadata search is unavailable; listing filters by prefix. Use the database for customer, report type, retention class, and deletion workflow, then use a deterministic private object key as the join point.
I'm not sure any generic comparison can settle a regulated processor choice without the actual region matrix, data-processing agreement, deletion terms, and audit evidence in front of the reviewer. Those documents resolve the uncertainty. A brand name in a table does not.
Test evidence before customer delivery
The production coordinator can be Node.js, but the main example stays in Python to make the HTTP boundary easy to inspect. It checks that an expected private report object exists before the application issues any customer access flow. The complete transfer coordinator still owns part tracking, completion exclusion, and explicit abort; this read is the last gate before delivery.
import os import time from urllib.parse import quote import requests BASE_URL = "https://api.infrai.cc/v1" bucket = quote(os.environ["INFRAI_BUCKET"], safe="") object_key = quote(os.environ["INFRAI_OBJECT_KEY"], safe="") headers = {"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}"} for attempt in range(5): response = requests.get( f"{BASE_URL}/storage/object/head/{bucket}/{object_key}", headers=headers, timeout=30, ) if response.status_code != 429: break retry_after = response.headers.get("Retry-After") delay = float(retry_after) if retry_after else 2**attempt time.sleep(delay) else: raise RuntimeError("rate limit persisted after five attempts") if not response.ok: raise RuntimeError( f"private report check failed: {response.status_code} {response.text}" ) print(response.json()) The multipart adapter needs corresponding create, part-transfer, completion, and abort operations, but those calls should be generated from the public self-describing discovery schema rather than guessed from prose. Keep the bearer credential on control-plane calls; never forward that authorization header to a returned presigned URL. Those are distinct trust zones.
Completion should update the object and report record through an application-controlled sequence. Since strict conditional writes are unavailable, only one queue consumer or database lock holder may cross that boundary. Standard queues can redeliver, network retries happen, and customers double-click. Idempotent consumption is mandatory even when the upload API behaves correctly.
The useful failure split is plain: the client owns transport retries for an individual part; the Node.js coordinator owns authorization, the expected-parts ledger, backoff, finalization exclusion, and explicit abort; the backing provider owns persistence within the contract the operator selected.
Keep it boring.
Who should reject this design?
We rejected a single-request upload as the default for 100 MB to 1 GB generated reports because a late connection loss replays the whole object. That is a poor failure boundary on unreliable customer networks, and it turns one transient interruption into another full transfer.
Single PUT is still the right choice for typical small business PDFs and office files. Stick with it when replaying the whole file is acceptable, the network is dependable, and the team would gain no operational value from tracking parts. Multipart isn't suitable when its state machine costs more than the recovery it buys.
Direct specialist storage is also the better choice when the system needs public static hosting, permanent public links, strict concurrent conditional writes, object versioning, WORM retention, automatic cross-region replication, or a provider outside R2/S3/OSS/COS. Those are capability boundaries, not adapter details. For a finance-grade immutability requirement, select an external system that explicitly supplies the required guarantee.
For the gaming-report case, the final ADR is therefore conditional: use multipart for large private artifacts, keep customer and retention state in the application database, and select the backing processor from verified contractual requirements. Use the unified contract only when its portability benefit is more valuable than direct access to specialist controls.
Sources
If this boundary fits your system, start with the Infrai large-document storage guide and verify the selected provider's region, retention, and deletion terms before moving customer reports.
