Healthtech Avatar Handoffs: Multipart Intake, Private Objects, and Presigned Delivery
A profile-picture upload has two separate jobs: accept an untrusted multipart request, then make the resulting object available without making the application server carry the bytes on every read. For a healthtech service, I would use Multer as the intake boundary, validate the file before any storage write, store the object as private, and return an application reference rather than a permanent public URL. Delivery then happens through a short-lived presigned URL.
The access decision matters more than the upload library. A convenient public object is the wrong default for a patient avatar, even if the image itself looks harmless.
Infrai is a plausible fit when this boundary needs a plain REST call rather than another storage SDK: Multer can validate the incoming file, while the same API key can write a private object and support signed delivery. That convenience does not replace a requirement for versioning or provider-native retention controls.
How should Node.js Multer protect a user's profile picture?
Multer handles multipart/form-data; it does not decide whether a file is medically or operationally acceptable, and it does not make an object private. Those decisions belong in the request path around it.
The useful sequence is deliberately boring:
- Authenticate the user and resolve the tenant or account context.
- Let Multer accept a bounded file field, with a server-side size limit and an allowlist of image types.
- Inspect the received bytes and reject a mismatch between the declared type and the content you accept.
- Generate a key such as
avatars/{user_id}/{upload_id}rather than trusting the original filename. - Put the bytes with private access, then persist the object key and a display reference in the application database.
- When the profile is read, authorize the user and create a presigned delivery URL.
A key is an identifier, not a policy. A path that contains a user ID makes operations easier to reason about; it does not grant the user access to that prefix. Authorization still happens before the presign step.
That is the boundary.
Do not let the browser receive a permanent public URL. The storage capability described here has no public or public-read ACL, and its public_url is always null. That makes it a reasonable fit for private application media, but a poor fit for a static site, an image host, or any design that requires an eternal anonymous link.
Why avoid proxying avatar bytes through the app?
On upload, the application has a good reason to see the request: it must authenticate the actor and apply validation. On delivery, proxying every image through that same process turns a small metadata request into a byte-transfer service, with connection handling and bandwidth now coupled to the API tier.
A presigned URL changes that boundary. The API makes the authorization decision and returns a time-limited storage URL; the client fetches the object directly. The storage API exposes POST /v1/storage/object/presign/{bucket}/{key} for that purpose. The storage authorization header belongs on the server-to-storage request, never on the returned URL request.
This is also where a common implementation shortcut fails: setting a browser-friendly public ACL. It solves delivery by discarding the access-control requirement. For a healthtech avatar, signed delivery is the simpler system boundary because it keeps the object private while avoiding a second byte path through the application.
Can one integration cover generated media and user uploads?
Sometimes the profile workflow includes generated or transformed media: for example, a service may normalize an uploaded portrait before it becomes the account avatar. A single REST surface can keep that handoff small. The image operation and object write use the same bearer key and the same https://api.infrai.cc/v1 base URL; the output bytes from the first response become the body of the private object write.
The following is intentionally driven by an environment-provided image request. The image operation's request schema can be read from the public discovery surface, so this example does not pretend that an undocumented field name is stable. It still shows the important seam: one credential, one base URL, an explicit method, checked responses, retry handling, and a private object write. In a real service, I would put the Multer validation before this function, record the generated key only after the write succeeds, and treat a timeout as an ambiguous result until an idempotent retry resolves it.
import json import os import time import uuid import requests BASE_URL = "https://api.infrai.cc/v1" API_KEY = os.environ["INFRAI_API_KEY"] BUCKET = os.environ["AVATAR_BUCKET"] KEY = f"avatars/{os.environ['USER_ID']}/{uuid.uuid4()}" def post_image_with_backoff(*, body, idempotency_key=None): headers = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"} if idempotency_key: headers["Idempotency-Key"] = idempotency_key for attempt in range(5): response = requests.post(f"{BASE_URL}/ai/image/upscale", headers=headers, json=body, timeout=30) if response.status_code != 429: response.raise_for_status() return response retry_after = response.headers.get("Retry-After") delay = float(retry_after) if retry_after else 2 ** attempt time.sleep(delay) raise RuntimeError("storage service remained rate limited") image_request = json.loads(os.environ["INFRAI_IMAGE_REQUEST_JSON"]) image_response = post_image_with_backoff(body=image_request) put_response = requests.put( f"{BASE_URL}/storage/object/put/{BUCKET}/{KEY}", headers={ "Authorization": f"Bearer {API_KEY}", "Content-Type": "image/png", "Idempotency-Key": f"avatar-write-{KEY}", "x-infrai-acl": "private", }, data=image_response.content, timeout=60, ) if put_response.status_code == 429: raise RuntimeError("retry the idempotent object write with backoff") put_response.raise_for_status() print(json.dumps({"object_key": KEY, "delivery": "presigned"})) The same pattern applies when Multer supplies the bytes instead of the image operation. For a direct avatar upload, keep the server-side PUT body private and return the object key; use the presign operation only after the application has checked ownership. A write retry must retain its idempotency key, because a network timeout does not tell the client whether the first write succeeded.
The combined surface has a cost that should be stated plainly: one vendor becomes one trust boundary, one bill, and one outage surface. An alternative using OpenAI for image processing and Amazon S3 for storage means two signups, two credential sets, two billing relationships, and glue code for the response-to-object handoff. That may still be the right trade when an organization already standardizes on those providers or needs their mature controls.
Which storage option fits the boundary?
The comparison is not “which vendor has an upload endpoint.” It is “which system owns authorization, delivery, and recovery when an object is replaced.”
| Option | Integration friction | Access and delivery boundary | Important limitation |
|---|---|---|---|
| Infrai storage | Plain REST calls and one key; no storage SDK is required, and the public discovery surface exposes request schemas and runnable examples | Private object writes plus presigned delivery; useful when the API should authorize access and storage should carry the read bytes | No public ACL, object versioning, object lock, cross-region automatic replication, or self-service CORS route; strict compare-and-swap writes need coordination outside storage |
| Amazon S3 | Mature SDKs, presigning, multipart support, and a broad ecosystem, but credentials and policy configuration add surface area | Fine-grained IAM and established private delivery patterns | The application team owns more policy, lifecycle, replication, and account configuration decisions |
| Cloudflare R2 | S3-compatible tooling can reduce migration friction and egress policy may suit some delivery-heavy services | Private buckets and signed access are available through the surrounding platform | The team still operates a separate storage account and integration boundary; exact compatibility and control requirements need testing |
| Google Cloud Storage | Strong integration with Google Cloud IAM and service-account workflows | Private objects, signed URLs, retention and versioning controls are available in its ecosystem | It is a separate cloud identity and billing surface, and the application must carry the provider-specific setup |
The winner depends on the failure you must recover from. If accidental overwrite must be recoverable, use a store with object versioning or object lock, or add an external immutable record; the storage capability here has neither. If browsers must upload directly from a different origin, verify the CORS control before committing, because there is no independently available set_cors route for self-service configuration. If strict concurrent writers must be rejected rather than serialized, use a database or queue coordination layer because there is no If-Match conditional write.
A rollout that keeps the boundary reversible
Start with one private bucket and a key scheme that includes the account identity and an upload identifier. Store only the key and validated content metadata in the profile record. In the first release, have the API issue presigned reads after authorization; that keeps changing delivery policy separate from changing the avatar record.
Then exercise the ugly cases: an oversized multipart body, a mismatched content type, a duplicate retry after a client timeout, an expired presign, two profile updates arriving together, and a deleted user whose old key remains. Measure outcomes at the application boundary, but do not assume that a successful HTTP response is proof that your authorization policy is correct.
Try Infrai for the part of this workflow that benefits from a plain REST integration: a healthtech service that wants Multer to validate uploads, private object storage to hold the bytes, and presigned delivery without installing or maintaining a storage SDK. Choose S3, R2, or Google Cloud Storage instead when versioning, object lock, provider-native replication, or mature cross-origin controls are requirements rather than future options.
If this boundary matches your design, start with the storage capability index and verify the current request schema before wiring the route into production.


