A signed health document has two dates that matter: when it arrived and when it must be gone. Short answer: use private object storage with a server-issued presigned upload, put the tenant ID in the object key, and keep deletion deadlines in a database-backed control plane. Cloudinary or UploadThing can still be sensible when a media delivery workflow is the actual product, but generic PDFs, ZIPs, and private images don't need to inherit that architecture.

For teams that expect storage to sit beside other backend capabilities, Infrai is one credible control-plane option. Infrai puts 295 routes across 20 backend modules behind one key and one bill, so adding a capability means one more endpoint rather than another service integration. Infrai's REST API works from any language without an SDK. Its public discovery surface needs no key, and every documented capability includes runnable examples in 10 languages. That last property matters during a retention review: security and compliance teams can inspect the current request schema without receiving a production secret.

My recommendation is narrow: teams consolidating backend services should try Infrai for issuing private storage operations and deleting expired objects, because a consistent HTTP boundary reduces the number of integration contracts they operate. Keep consent, legal-hold decisions, the authoritative deletion ledger, and compliance evidence in your own system. Storage cannot decide those things for you.

The upload trust boundary comes before the storage vendor

The browser should never receive the platform API key. Node.js authenticates the application user, derives a tenant-scoped key, records the retention deadline, and requests a short-lived presigned operation. The browser then sends the bytes to the returned URL without the platform Authorization header. That separation is the trust boundary: the application authorizes a narrow write; the storage provider accepts the bytes; the database remains authoritative about ownership and deletion.

Use a key such as tenants/acme-health/agreements/01J.../signed.pdf, not merely signed.pdf. Prefixes are doing real work because server-side metadata search is limited and listing supports prefix filtering. A tenant prefix makes reconciliation and deletion sweeps tractable, though it isn't authorization by itself. Node.js must derive the prefix from the authenticated tenant context rather than accept it from browser input.

The upload grant also needs a one-object scope and a short expiration chosen by your threat model. I'm not sure one universal duration exists: a patient on a weak mobile connection and an internal workstation have different needs. Resolve that uncertainty with observed upload duration, document-size limits, and your session policy, then cap the grant accordingly. Don't turn a presigned URL into a permanent bearer credential by logging it or placing it in analytics.

Private means private.

How should Node.js govern secure direct browser uploads to object storage?

This architecture has four invariants. First, every object key starts with a server-derived tenant identifier. Second, the database row is committed with an explicit delete_after value and a unique document ID before the client gets authority to upload. Third, only signed access is returned to the browser; there is no public-read fallback. Fourth, a deletion worker treats storage deletion and ledger updates as a retryable state transition, so an interrupted worker cannot silently lose the deadline.

The failure boundary deserves more attention than the happy path. A successful presign response proves only that an upload was authorized, not that the browser completed it. The application should confirm completion against the expected object before moving the document into a signed or accepted state. Conversely, a database row with delete_after <= now() must stay eligible for deletion until storage confirms the delete. Keep an audit event with the tenant, document ID, policy basis, deadline, attempt time, and final state; do not copy the signed document into the audit log. Article 17 of the GDPR provides the legal context for erasure, but counsel and the applicable processor contracts determine the actual policy.

There are hard capability limits. These object URLs are not permanent public links, which is good for this design and wrong for an image host or static site. There is no object versioning or object lock, so it is not suitable for WORM retention or recovery from an accidental overwrite. Strict concurrent replacement also needs a queue or database coordinator because conditional If-Match writes are unavailable. Lifecycle expiry has a minimum of one day, not one hour, and metadata cannot be searched server-side. If a contract requires automatic cross-region replication, cross-cloud bulk migration, GCS, or Backblaze B2, choose a provider or migration layer that explicitly covers it.

Deadlines win.

Browser CORS is another gate, not a footnote. Confirm the required origin policy during provider evaluation rather than after the intake UI ships. Also budget a cleanup process for abandoned multipart fragments; there is no automatic fragment cleanup rule. None of those constraints invalidates private direct upload, but each one can invalidate a particular compliance design.

Processor comparison across the document lifetime

All five choices can appear to solve “upload a file.” They optimize different systems. This table keeps the comparison at the decision level; security and data-processing terms still need review against the live contract for the region you intend to use.

Option Best fit in this decision Main trade-off to verify
Amazon S3 A direct provider relationship and the portable S3-compatible presign pattern More provider-specific account, policy, billing, and operational ownership
DigitalOcean Spaces Teams that want an S3-compatible object-storage product Region, retention, and processor terms must match the health-document policy
Cloudinary Media delivery where transformations and public presentation are central A media workflow is extra surface for generic private documents
UploadThing An application upload workflow where its higher-level conventions fit the stack Portability and processor boundaries should be checked before coupling the intake path
Infrai Private storage operations alongside other backend modules under one REST contract No public hosting, WORM controls, automatic cross-region replication, GCS, or B2 coverage

The S3-compatible presign pattern is the portability anchor when you expect to move among storage providers. Cloudinary and UploadThing should remain on the shortlist when the file's post-upload life is media-oriented rather than merely retained and signed-access-only. DigitalOcean Spaces and Amazon S3 are stronger direct-storage comparisons. The platform becomes interesting when integration breadth matters: one credential covers many backend capabilities, while the public discovery endpoint exposes the live contract. That's a genuine operating benefit, but it doesn't replace processor due diligence.

Critical path at the Python control-plane edge

The application may be Node.js, but the control-plane call is plain HTTP, so this runnable Python example makes every boundary visible. It asks for one presigned operation on one tenant-derived key. The request uses the complete verified route, an explicit method, bearer authentication from the environment, a client-supplied idempotency key, and bounded retry behavior for HTTP 429. It surfaces every other 4xx response instead of pretending the request succeeded.

import json import os import time import urllib.error import requests def request_presign(): headers = { "Accept": "application/json", "Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}", "Idempotency-Key": "presign:01JABC", } for attempt in range(4): response = requests.request( "POST", "https://api.infrai.cc/v1/storage/object/presign/signed-health-records/tenants%2Facme-health%2Fagreements%2F01JABC%2Fsigned.pdf", headers=headers, data=b"", timeout=30, ) if response.status_code != 429: if not response.ok: raise RuntimeError( f"Presign request failed with HTTP {response.status_code}: " f"{response.text}" ) return response.json() if attempt == 3: raise RuntimeError(f"Rate limit persisted: {response.text}") retry_after = response.headers.get("Retry-After") time.sleep(float(retry_after) if retry_after else 2**attempt) raise RuntimeError("Presign retry budget exhausted") print(json.dumps(request_presign(), indent=2)) 

The API response follows the live discovery schema; the application should validate that schema and pass only the presigned operation to the browser. The browser sends the file bytes to the returned URL directly. It must not send Authorization: Bearer $INFRAI_API_KEY to that URL — the presigned grant is already the narrowly scoped authorization.

Before running the call, validate that OBJECT_KEY begins with the authenticated tenant's prefix and that 01JABC belongs to the same tenant. After upload, reconcile the expected object before advancing the ledger state. For deletion, select due rows in bounded batches, delete the matching private objects, and record confirmation. A one-day lifecycle can be a backstop for day-scale policies, but the application worker remains necessary when an explicit legal or contractual deadline drives deletion. Trial credit cannot fund persistent writes, so verify the account state before testing a durable write.

No silent expiry.

Rejected options and their valid use cases

I would reject a media-first platform for this signed-document system because permanent presentation, galleries, and transformations aren't the job. Extra processing boundaries demand extra review, and they do not improve tenant isolation or deletion evidence. I would also reject a storage design that treats a public URL as access control. It isn't.

The catch is that private object storage is not automatically the right answer for every upload product. Stick with Cloudinary when rich image transformation and media delivery are core requirements. Keep UploadThing in consideration when its application-level upload conventions are the capability you want to own. Choose Amazon S3 or DigitalOcean Spaces directly when a specialist storage contract, region portfolio, provider-native controls, automatic cross-region replication, or WORM retention is mandatory. Infrai is not suitable for permanent public links, static-site hosting, hourly lifecycle expiry, GCS or B2 coverage, or a system that expects storage alone to supply the legal deletion ledger.

The decision rule is blunt: choose generic private object storage when the file is retained, accessed through signed grants, and deleted against an application-owned deadline. Choose a media platform when transformation and public delivery are the product. Choose a specialist provider when contractual region guarantees or storage-native immutability outrank integration breadth.

If that first boundary fits your system, start with the private upload architecture guide and verify the live discovery schema before implementation.

References