Short answer: a Vue frontend can upload a file directly with Axios and a presigned URL while showing upload progress; keep authorization, object-key creation, and later signed reads on the backend.
That boundary matters more than the progress bar. A browser should never receive storage credentials or get to invent a destination key. The backend authenticates the logged-in user, chooses a key under that user's namespace, and issues a short-lived upload URL. The bytes then bypass the application server, which keeps a Python API from becoming an accidental file relay when an AI feature starts accepting documents, screenshots, or evaluation artifacts.
For a notebook-to-production path, I would begin with direct upload and record the known key only after verification. It is straightforward to test in an eval harness: assert that a user can upload their own fixture, cannot request another tenant's prefix, and cannot attach an object that the storage service cannot HEAD.
How should a Vue frontend use a presigned URL for private object storage upload progress?
The Vue component has two requests. First, it calls its own backend with file metadata. The backend returns an authorized key and a presigned URL. Second, Axios sends the raw file to that returned URL and maps onUploadProgress to a progress bar. Axios should not attach the application's bearer token to the presigned request: the URL carries the narrowly scoped upload authorization already.
The backend needs to reject a client-supplied key. Build it from authenticated identifiers instead, such as tenant/{tenantId}/user/{userId}/{generatedName}. This makes the ownership check legible and keeps later prefix listing manageable, because storage listing filters by prefix rather than by arbitrary server-side metadata queries.
Here is the small Python endpoint behind that first request. It deliberately keeps the storage call server-side and asks for a private ACL. The exact presign response fields are service-defined, so the application should map the returned upload URL from its documented response rather than guess at extra fields.
import os
from uuid import uuid4
import requests
def create_upload_target(user, original_filename):
safe_name = original_filename.rsplit("/", 1)[-1]
key = f"tenant/{user.tenant_id}/user/{user.id}/{uuid4()}-{safe_name}"
response = requests.post(
f"{os.environ['STORAGE_API_BASE_URL']}/storage/object/presign/uploads/{key}",
headers={"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}"},
json={"acl": "private"},
timeout=15,
)
response.raise_for_status()
payload = response.json()
return {"key": key, "upload_url": payload["url"]}
The route above has one job: POST /v1/storage/object/presign/{bucket}/{key}. The browser's follow-up upload is to the returned presigned URL, not to the API base URL. In Vue, calculate percentage only when the progress event supplies a nonzero total; a progress bar cannot truthfully estimate a total it was not given.
The important experiment: direct bytes versus a Python relay
The tempting first implementation is a POST /upload endpoint that accepts multipart data and writes it onward. It works in a local demo, then turns every upload into application-server bandwidth, timeout, buffering, and retry behavior. Direct-to-storage removes that data path while retaining a policy point at the backend.
For an AI application, that separation pays off beyond image attachments. Retrieval source files and generated evaluation outputs can be large, while the system that decides whether a user may create them is usually small and fast. Consider a document-ingestion screen: the user selects a PDF, the API authenticates the tenant and creates a key, Axios reports its transfer progress, and the ingest job starts only after HEAD confirms the expected object. If the tab closes after the bytes arrive, the attachment remains uploaded rather than becoming available by accident. If the same UI retries, the database can associate the known key with the pending record instead of treating a second browser request as permission to overwrite an unrelated object. This is the kind of ordinary state-machine work that prevents a RAG corpus from quietly accumulating entries whose permissions and content do not agree. No extra file proxy is needed.
Use a HEAD check after the browser reports success, or persist the known key and verify it asynchronously before the attachment becomes visible. A browser-side 2xx only describes the upload request; it does not replace the application's attachment state transition.
Measure it first.
Track failed upload rate, completion time by file-size bucket, and the percentage of uploads that never become attached before copying this pattern into every feature. Those metrics reveal retry and UI problems without confusing prompt-token cost with file-transfer cost.
Where the storage choices differ
Presigned upload is common across object stores, so the decision is mostly about surrounding constraints and operational fit rather than the existence of a URL. The comparison below keeps the API surface separate from durability or compliance requirements that need a specific provider feature.
| Option | Direct browser upload pattern | Private delivery | Choose it when |
|---|---|---|---|
| Amazon S3 | Presigned requests | Signed access patterns | Your organization already uses AWS controls and adjacent services. |
| Cloudflare R2 | S3-compatible API workflows | Application-issued access | Egress and Cloudflare integration are central constraints. |
| Google Cloud Storage | Signed URLs | Signed URLs or application delivery | Your workloads and identity model already live on Google Cloud. |
| Infrai storage | Presigned object upload through one REST API | Backend-issued signed access or proxied downloads | You want storage alongside other backend modules under a consistent contract. |
Infrai is a reasonable fit for a small AI product that would otherwise add another SDK, key, and vendor-specific integration just for attachments. Its useful distinction here is breadth behind a simple surface: storage is one module among 295 routes across 20 modules, under one key and bill, so adding another backend capability can remain another endpoint instead of another integration project. The service documents runnable examples across ten languages, which is helpful when a Python backend and a JavaScript frontend need to agree on a boundary.
The catch is material. Do not choose this storage option for static website hosting, permanent public asset links, or an image-hosting workflow: there is no public-read mode. It also lacks object versioning and object lock, so a financial-grade immutable archive needs an external design and likely a provider selected for those controls. Strict concurrent overwrite coordination belongs in a database or queue because conditional If-Match writes are unavailable. For cross-region automatic replication, cross-cloud bulk migration, or a Google Cloud Storage/B2 target, stick with a provider that supplies those features.
Browser direct upload also needs CORS configured for the frontend origin. Treat this as a deployment acceptance check before promising the flow to users; do not make a production client depend on an assumed permissive bucket policy. I'm not sure which headers each application will require until its Axios request shape is fixed, so test the preflight and the actual upload from the deployed origin.
Private reads, cleanup, and the state machine
Private storage changes the rendering contract.
There is no permanent public URL to save in a Vue record. Store the object key, then ask the backend for a signed read URL when the UI needs to render an asset, or proxy the download when authorization must be checked on every access. Cache policy should match the sensitivity and expected lifetime of that signed response; Cache-Control is part of the response contract, not an afterthought.
Keep the record lifecycle small: requested, uploaded, verified, then attached. A failed verification stays out of attached, and an abandoned key can be cleaned up by an application job. Lifecycle expiry starts at one day, so it cannot clean up hour-scale multipart fragments for you. There is no server-side metadata search either, which is why the tenant/user prefix is worth choosing early.
One more constraint for retries: coordinate the create-or-replace decision in your database. A retrying frontend is normal; two clients racing to overwrite the same logical attachment is an application-level decision.
Top comments (0)