Compare Cloud Storage Providers for Direct Browser Uploads: Node.js Access Controls
The hard part of direct browser upload for logistics backups is not moving bytes. It is proving, during a restore, that the selected snapshot belongs to the requesting tenant and is complete enough to use. Delivery simpl
The hard part of direct browser upload for logistics backups is not moving bytes. It is proving, during a restore, that the selected snapshot belongs to the requesting tenant and is complete enough to use. Delivery simplicity matters, but I would spend that simplicity budget at the storage adapter boundary: let the browser write to a short-lived, server-derived object key, then let the application decide when a snapshot becomes restorable.
Short answer: model each backup as an application-owned operation with a tenant, snapshot identifier, expected checksum, and explicit state. Compare S3, Cloudflare R2, and Wasabi by how well the same browser, interruption, and recovery tests preserve that model. A presigned URL is only the handoff; it is not the restore authorization.
The incident lesson is a state boundary
Take a bounded production scenario: a logistics tenant uploads a 12 GiB nightly backup from a browser, the transfer finishes at the storage service, and the browser tab closes before the application receives completion. The object exists. The snapshot is not ready. If a restore screen discovers objects by prefix, it can present an unverified upload as a selectable recovery point.
The invariant is narrower and safer: the database record owns the snapshot, while storage holds bytes addressed by a server-derived key. The restore path selects a snapshot record, checks its tenant and state, and only then obtains a read grant. It never treats a filename, bucket listing, or browser callback as proof of ownership.
The upload is not the snapshot.
That design makes the trade-off visible. A single upload endpoint is easy to explain, but it pushes authorization and reconciliation into an ambiguous callback. A stateful operation adds records and a worker, yet gives on-call engineers something concrete to inspect when the browser disappears. For backups, the latter is the less expensive complexity.
Which failure should the restore workflow tolerate?
Start with recovery, not with a provider feature matrix. A snapshot should move through states such as created, uploading, verifying, restorable, and expired. The application should store the tenant ID, object key, size expectation, checksum expectation, creation time, and the actor allowed to request a restore. A missing completion event is then an unverified operation, not a mysterious storage outage.
The restore request must be idempotent. Two clicks should not create two restore jobs, and a retry after a timeout should not silently switch the selected snapshot. Record the snapshot ID in the restore command and re-check its tenant at execution time; authorization at page-render time is too early for a queue that may run later.
I initially frame this as a delivery problem; the correction is to treat it as a recovery contract. The 12 GiB example is a capacity-planning boundary, not a performance claim: the useful question is which state and evidence survive a browser timeout, a worker retry, and a later restore request.
The following Go example shows the decision point without binding the application to a provider SDK:
package backup
import "errors"
var (
ErrForbidden = errors.New("snapshot is not owned by tenant")
ErrNotReady = errors.New("snapshot is not restorable")
)
type Snapshot struct {
ID string
TenantID string
ObjectKey string
State string
}
type RestoreRequest struct {
TenantID string
SnapshotID string
}
func AuthorizeRestore(req RestoreRequest, snapshot Snapshot) error {
if snapshot.TenantID != req.TenantID {
return ErrForbidden
}
if snapshot.State != "restorable" {
return ErrNotReady
}
if snapshot.ObjectKey == "" {
return ErrNotReady
}
return nil
}
The code intentionally does not create a URL. Signing belongs behind an adapter after this check. That separation is useful for SLOs: the restore authorization decision can be measured independently from storage request latency, and a storage timeout cannot be mistaken for a tenant mismatch.
How should teams compare cloud storage providers for direct browser uploads?
Use one conformance suite against all three targets. The suite should exercise an actual browser origin, an expired grant, a mismatched tenant key, an interrupted upload, a completed upload with a lost callback, and a restore of a specifically selected snapshot. MDNβs CORS guidance matters here because a command-line upload test does not prove that the browserβs preflight and requested headers are accepted.
| Boundary | Required evidence | Why it affects recovery |
|---|---|---|
| Authorization | The key is derived from a server-owned tenant and snapshot record | A valid signature can still protect the wrong object |
| Browser delivery | CORS allows the intended origin and headers only | A server-side green test may hide a browser failure |
| Completion | The adapter can inspect the object and record a durable transition | Bytes without state must not enter the restore catalog |
| Interruption | Multipart work can be completed or aborted and later reconciled | A 12 GiB transfer should not become permanent orphan state |
| Retrieval | The restore worker receives a grant for the selected key | Listing-based discovery can cross tenant boundaries |
S3 documents multipart upload as an initiate, upload-parts, and complete-or-abort workflow. That is a useful reference contract for large backups, but the application should test the same lifecycle through each adapter rather than infer equivalence from S3-compatible request shapes. The comparison is operational: S3, Cloudflare R2, and Wasabi are three targets for the same tenant and browser matrix, with exceptions recorded at the adapter boundary.
This is where I would keep the roadmap honest. If the first release has a tested single-object size limit, it can defer multipart state. If the backup distribution makes retries necessary, multipart earns its additional records and cleanup path. The threshold belongs in capacity planning and SLO review, not in a vendor slogan.
What does a trustworthy restore need after upload?
A verifier should compare the stored object with the snapshot record before changing verifying to restorable. It should record the result, the object size, the checksum evidence available from the storage interface, and the transition timestamp. The browser can report success, but the server must make the publication decision.
The reconciliation worker should scan bounded time windows or use a cursor. It can classify an old uploading record as abandoned, retry a transient inspection failure, or quarantine a key that does not match the server-owned record. It should never βrepairβ a tenant mismatch by rewriting ownership. That is an access-control event.
The failure chain deserves a concrete test because each step can look healthy in isolation: the browser receives a successful storage response, the completion request times out, the worker sees an object during its next scan, and an operator retries the transition while the original request is still in flight. Without an idempotency key tied to the snapshot operation, that retry can create duplicate restore work; without a state check, it can publish bytes before verification; without a tenant check at execution time, a queued command can outlive the session that authorized it. I would record those transitions as separate events and put a bounded retry policy around inspection, because an SLO for βbackup upload succeededβ hides the more important measure: how long it takes before a selected snapshot is actually restorable. The exact timeout and retry count belong to the teamβs capacity model and storage tests, not to a generic provider comparison.
That is the recovery boundary.
Observability should follow the operation rather than the presigned URL. Log the tenant ID, snapshot ID, operation type, state transition, expiration time, and a hash of the key; do not log the full URL. For the SLO, separate grant issuance, browser transfer, verification, and restore-start latency. One aggregate upload metric cannot tell an operator whether the problem is CORS, storage delivery, verification, or queue delay.
Retention belongs in the same model. Lifecycle rules can clean up predictable old objects or incomplete multipart work where the backend supports that policy, but an application still needs to represent legal holds, failed verification, and audit evidence. Keep the audit record after an object expires so support can explain what existed and who was authorized to request it.
A portable decision rule for the platform roadmap
Choose the simplest delivery path that preserves the restore invariant. For smaller backups, a single object may be adequate within a tested timeout and size envelope. For larger transfers, multipart is justified only when its retry and cleanup benefits outweigh the extra state. A server-side intake remains the better shape when every byte must be inspected, transformed, or redacted before it is allowed into the backup store.
The adapter should expose a small generic surface such as Put, Complete, Abort, Head, and Delete. The controller should request an operation for an existing snapshot record; it should not assemble a tenant prefix or signature from browser input. That boundary keeps delivery replaceable without pretending that all storage systems have identical failure behavior.
The durable decision is therefore not a ranking of providers. It is a testable contract: tenant ownership is decided by the application, completion is evidenced before publication, selected snapshots are re-authorized at restore time, and retention leaves an audit trail. When those conditions hold, delivery simplicity is useful. When they do not, a simpler upload path only makes the next recovery harder to explain.
Sources
References:
- MDN, βCross-Origin Resource Sharing (CORS)β: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS
- AWS, βMultipart upload overviewβ: https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpuoverview.html
- AWS, βObject lifecycle managementβ: https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lifecycle-mgmt.html
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.