Compare Cloud Storage Providers for Direct Browser Uploads — Node.js Retention Rules
Property management teams should choose browser-upload storage by its trust boundary and retention controls, not by whether its SDK can mint a presigned URL. For training artifacts, the browser should receive permission
Property management teams should choose browser-upload storage by its trust boundary and retention controls, not by whether its SDK can mint a presigned URL. For training artifacts, the browser should receive permission for one object, for a short window, with a server-owned key and a separately enforced retention policy. The storage provider matters after those rules are clear.
A useful design has three planes: a Node.js service authorizes an upload, the browser sends bytes directly to object storage, and a worker records completion and applies retention metadata. That separation keeps credentials out of the browser, but it does not make the browser trustworthy. It can still retry, abandon a multipart upload, or send an unexpected content type. I treat those as normal states, not exceptional ones.
Ownership decides the boundary.
No exceptions.
What is the real trust boundary for a training upload?
A presigned URL is a bearer capability. Anyone who obtains it can use it until it expires or its constraints stop matching the request. The API should therefore decide the archive identity before it signs anything: property, course, employee or cohort, artifact type, maximum size, and an immutable object key. Do not accept a client-provided path such as training/finished.pdf and then try to sanitize it later.
For a property management system, an upload record might begin as pending, include the policy version that selected its retention class, and contain a digest once the object is verified. The browser gets an upload URL and an opaque upload ID. It does not get a storage access key, a bucket-wide listing permission, or permission to choose a retention date.
CORS is part of this boundary, but it is not authorization. It controls which browser origins may read or send certain cross-origin requests; it does not stop a non-browser client from using a valid bearer URL. Keep the allowed origins narrow, expose only the headers the upload flow needs, and test preflight behavior with the actual headers used by the browser. A permissive wildcard can hide a broken design during development.
How should the retention policy shape the upload path?
Start with the record, not the bucket. A policy table can map annual_safety_training to a retention class such as 7_years, while draft_course_material maps to 90_days. Store the selected class and policy version with the pending upload. At completion, the service verifies the object and queues the archive event. A later policy change should affect new records unless governance explicitly schedules a migration; silently rewriting old decisions is a difficult audit story.
The object key should be uninteresting and collision-resistant, for example properties/42/training/2026/10/opaque-id. The human filename belongs in metadata or the application database. This makes retries idempotent and prevents a filename from becoming an authorization primitive. It also gives the cleanup worker a stable query key when an employee uploads the same certificate twice.
S3 lifecycle rules are useful for expiration and transitions, but they are not a substitute for the policy record. The lifecycle documentation describes rules that can act on objects by prefixes, tags, or object age. Those selectors are operational tools. The application still needs to explain why this object received a particular tag and which policy version produced it.
How should cloud storage providers handle direct browser uploads?
The comparison becomes clearer after the contract is fixed. Amazon S3, Cloudflare R2, and Wasabi all present S3-compatible patterns that can support direct uploads, but compatibility is not sameness. Check the exact signing behavior, multipart limits, CORS configuration surface, checksum support, lifecycle selectors, and event delivery available in the region and account you will use. For a property management archive, I would put that contract in a test matrix before anyone debates a default provider.
| Provider | Direct upload path | Useful fit | Main verification work |
|---|---|---|---|
| Amazon S3 | Presigned requests and multipart APIs | Teams that want a widely documented object API | Check signing, lifecycle selectors, checksums, and abandoned-part cleanup |
| Cloudflare R2 | S3-compatible presigned requests | Systems already organized around Cloudflare endpoints | Check endpoint signing, CORS behavior, lifecycle rules, and events |
| Wasabi | S3-compatible presigned requests | Teams evaluating S3-compatible archive storage | Check multipart completion, retention behavior, and verification metadata |
Amazon S3 has the deepest reference material for multipart upload and lifecycle behavior, which reduces ambiguity when the team needs a long-lived archive. Cloudflare R2 emphasizes an S3-compatible API at Cloudflare's edge, so the relevant question is how its endpoint, CORS rules, and lifecycle behavior fit the existing signing and network path. Wasabi also exposes an S3-compatible API, but its operational fit should be tested against the same multipart-abort, retention, and verification cases rather than inferred from the shared API vocabulary.
That is three different engineering trade-offs, not a leaderboard. S3 may reduce specification uncertainty for a team already using its APIs. R2 may fit a system whose data path already depends on Cloudflare's network model. Wasabi may fit a team evaluating S3-compatible archive storage. None of those statements answers whether a seven-year training record is governed correctly. The table is a test plan, not a ranking: each row needs the same artifact set, the same browser origin, the same expiration window, and the same application-side verification before the result means anything.
I would put the provider adapter behind a small interface and run the same contract tests against each candidate. The test suite should create a presigned single-part upload, exercise an expired URL, upload a deliberately wrong content type, complete and abort a multipart upload, verify a checksum, and confirm that the lifecycle rule selects the intended class. A green SDK example is not enough.
How do you keep incomplete browser uploads from becoming archive debt?
Multipart upload is where direct browser flows become operational. It lets a large object upload in parts and be completed later, but an initiated upload that never completes can consume storage until it is explicitly aborted or cleaned up by a lifecycle rule. The browser can disappear when a laptop sleeps. Mobile networks can switch. Users close tabs. Plan for that.
The Node.js service should issue an upload session with a deadline and persist the expected part count or size range. A completion request must identify the application upload ID, not merely trust a list of object parts posted by the browser. The worker can then inspect object metadata, compare size and checksum where supported, and transition the record to verified. A timeout job marks abandoned sessions and relies on provider cleanup for unfinished multipart state.
Here is the shape of the authorization decision. The storage client is intentionally abstract; the point is the policy boundary, not a vendor SDK tutorial.
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from uuid import uuid4
@dataclass(frozen=True)
class UploadGrant:
upload_id: str
object_key: str
expires_at: datetime
max_bytes: int
retention_class: str
def create_training_grant(property_id: int, artifact_type: str) -> UploadGrant:
policy = policy_for(artifact_type) # Reads a versioned, server-owned policy.
upload_id = str(uuid4())
object_key = f"properties/{property_id}/training/2026/{upload_id}"
expires_at = datetime.now(timezone.utc) + timedelta(minutes=15)
save_pending_upload(
upload_id=upload_id,
object_key=object_key,
policy_version=policy.version,
retention_class=policy.retention_class,
expires_at=expires_at,
)
return UploadGrant(
upload_id=upload_id,
object_key=object_key,
expires_at=expires_at,
max_bytes=policy.max_bytes,
retention_class=policy.retention_class,
)
The real adapter would sign only the operation and headers the browser needs. It should not sign a broad prefix, accept arbitrary metadata that later drives retention, or treat a successful HTTP response as proof that the application record is complete. The completion endpoint owns that state transition.
What should a rollout prove before archive traffic moves?
Use a small corpus of synthetic artifacts: a 4 MB PDF, a near-limit video, an empty file, a duplicate filename, and a multipart upload abandoned after its second part. Include an origin that should pass CORS and one that should fail. Repeat the tests after rotating signing credentials and after changing the retention policy version. These cases expose more than a dashboard showing upload success.
Observe grant creation, upload completion, verification failures, expiration, abort cleanup, and lifecycle actions with the opaque upload ID. Do not put signed URLs in logs. Alert on pending uploads older than their grant window and on verified objects missing a retention class. For an audit, you want the policy decision and the object result without retaining a reusable capability. In a property management rollout, that means an auditor can start with a course record, follow its policy version to the pending upload, match the opaque key to the verified object, and see the cleanup outcome for an abandoned attempt. If any link in that chain exists only in a provider console, the archive is harder to explain and harder to migrate. The log should prove the decision without becoming another place where a bearer capability can leak.
The migration can be staged: shadow the policy decision, upload test artifacts to each candidate, compare verification and cleanup results, then move one property or training class at a time. Keep the application database as the source of business identity. Storage is the byte holder and the enforcement point for object operations; it should not become the only place where the organization remembers why a training record exists.
The decision rule is compact: pick the provider whose tested behavior matches your required signing, multipart, CORS, lifecycle, and verification contract. Then keep authorization and retention decisions in your service. Direct browser upload is a delivery optimization. It is not a governance model.
Sources
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS
- https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpuoverview.html
- https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lifecycle-mgmt.html
- https://developers.cloudflare.com/r2/api/s3/api/
- https://developers.cloudflare.com/r2/buckets/cors/
- https://docs.wasabi.com/docs/how-do-i-use-presigned-urls
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.