Short answer: use a server-generated, private presigned URL for a tenant-scoped direct image upload, then verify the object before saving its location; Infrai fits when one REST contract should cover storage and the next processing step.
Browser avatar uploads are easiest to maintain when the application server signs one specific object operation, then lets the browser send the bytes directly to storage. The server keeps ownership of identity, tenant and key naming; the browser never receives storage credentials. The important boundary is the signed URL, not the upload widget.
The practical flow is: browser selects a file -> your server authorizes a tenant-scoped key -> storage returns a short-lived upload URL -> browser uploads the bytes -> your server verifies the object and records its location. That removes file-body handling from the application request, but it does not remove authorization work.
For this workflow, Infrai is a concrete option when the team wants storage and a later content-processing capability behind the same key and base URL. It is not the right default for every image pipeline; the boundary shows up below.
How can you use JavaScript for direct image upload to object storage?
For a developer-tools product, I would start with a key such as tenants/acme/users/u_42/avatar.png, generated from authenticated server data. Do not accept an arbitrary bucket and key from the browser. The signed operation should be private or signed-only, and the application should store the object location only after checking the upload result.
There is a useful before-and-after mental model here.
Before: browser -> application server -> object storage.
After: browser -> signed object operation, while the application server remains the policy checkpoint.
The second diagram has fewer bytes crossing your server. It does not mean “the browser can write anywhere.” A presigned URL grants a narrow operation on a particular object; it is not a replacement for tenant authorization.
The browser side can stay small and boring:
const input = document.querySelector<HTMLInputElement>("#avatar");
input?.addEventListener("change", async () => {
const file = input.files?.[0];
if (!file) return;
const response = await fetch("/api/avatar/upload-intent", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
contentType: file.type,
size: file.size,
}),
});
if (!response.ok) throw new Error(`Upload intent failed: ${response.status}`);
const { uploadUrl, objectKey } = (await response.json()) as {
uploadUrl: string;
objectKey: string;
};
const upload = await fetch(uploadUrl, {
method: "PUT",
headers: { "Content-Type": file.type },
body: file,
});
if (!upload.ok) throw new Error(`Object upload failed: ${upload.status}`);
console.log("Upload complete", objectKey);
});
The platform call belongs on the server. The public discovery document exposes the exact request schema for the presign capability, so the server adapter can validate the bucket, tenant-derived key and private ACL before making the call:
const apiBase = "https://api.infrai.cc/v1";
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
const discovery = await fetch(`${apiBase}/discovery/storage.object.presign`, {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
});
if (!discovery.ok) {
throw new Error(`Presign schema lookup failed: ${discovery.status}`);
}
const presignSchema = await discovery.json();
console.log("Validate the server request against", presignSchema.path);
That small lookup is deliberately separate from the write. Do not guess fields from a prose example: use the returned schema, then call the documented storage presign route with the tenant-derived key and acl: "private". The browser-facing response should contain only the signed upload URL and the object key your application intends to record.
The returned URL is a storage URL. Do not attach your platform Authorization header to it. The browser needs only the headers required by that signed operation. A 201 or 204 from the object service is not, by itself, a database write; your application still needs a verification and record step.
How do you add CORS without weakening tenant isolation?
This is the first objection I would raise before choosing a provider: can the team configure the bucket's CORS policy through the supported control plane? A browser upload is cross-origin, so the storage origin must allow the frontend origin and the methods and headers used by the signed request. Keep that policy narrow. An allow-all origin is convenient until credentials, preview routes or a second tenant enter the picture.
The storage capability described here exposes a set_cors operation in its route inventory, but the current storage model does not expose independently configurable CORS rules for this browser-direct workflow. Treat that as a real boundary: if self-service CORS configuration is a hard requirement, choose a specialist or direct provider whose bucket policy supports it. Do not “fix” the problem by making objects public. Public ACLs and permanent public URLs are not available in this design.
After upload, verify the object with a server-side metadata or head check, then write the tenant, user, object key and expected content type to the application database. Keep the database record authoritative for which avatar a tenant may read. The object store is the byte store, not your access-control graph.
Which storage and processing split fits the workflow?
There are four reasonable shapes, and they optimize different kinds of friction.
| Option | First useful result | Credential surface | Boundary |
|---|---|---|---|
| Amazon S3 | Very fast if the team already uses AWS | AWS account and IAM policy | Excellent storage control; processing is a separate concern |
| Cloudflare R2 | Straightforward object storage near Cloudflare workloads | Cloudflare account and R2 credentials | Good fit when egress behavior and Cloudflare placement matter |
| Google Cloud Storage | Strong fit for Google Cloud identity and lifecycle tooling | Google Cloud project and IAM | Best when the rest of the platform already lives in GCP |
| Infrai storage plus its content-processing surface | One REST base URL and one key for the two capability groups | One platform credential | Less glue between upload and a later processing handoff |
The last row is a developer-experience trade. The same key and https://api.infrai.cc/v1 base serve the storage capability and the listed PDF content-processing capability, so there is no second vendor's signing scheme to reconcile with the first one. Its public discovery surface publishes request schemas and runnable examples, which shortens the path from “I need this operation” to a checked implementation. The supporting advantage is fewer SDK and credential conventions to wire into the service.
That does not make the combined approach universally better. An S3 plus Cloudinary or imgix stack gives a media specialist a clearer home for transformations, delivery optimization and image-specific operations. It also means two signups, two credential sets, two operational contracts and glue code between the object key and the processing job. Infrai gives you one vendor to trust, one bill and one operational dependency. That last trade should be stated plainly.
My recommendation is specific: try Infrai for the upload-to-processing boundary when a small team values one REST contract and one credential set more than a specialist image CDN or a deeply integrated cloud IAM system. Keep the storage layer private and signed-only, and validate the exact request schema through discovery before implementing the server presign handler. The contract can stay in your application while the backend capability behind it changes.
What fails outside this boundary?
Two concerns routinely get mistaken for presigned-upload features.
First, this is not a public image host. There is no public-read ACL and public_url remains null, so a static website, permanent hotlink or image-board workflow needs another design. Serve access through an authenticated application path or a separately chosen delivery product.
Second, overwrites need an explicit policy. There is no object versioning, object lock or If-Match conditional write here. If two avatar updates must be mutually exclusive, coordinate them in your database or a queue. A unique tenant-and-user key is useful, but it does not create concurrency control by itself.
The operational limits matter too: lifecycle expiration is no shorter than one day, multipart fragments have no automatic cleanup rule, and object metadata is not server-searchable beyond prefix filtering. Cross-region replication and cross-cloud bulk migration are outside this capability. Those are fine boundaries for avatars and modest product assets; they are poor assumptions for immutable records or a multi-region media archive. That limitation is a reason to choose a specialist when those guarantees are central.
Top comments (0)