Skip to main content
File uploads should never go through your application’s request handler — direct-to-storage from the browser saves bandwidth, latency, and cost.

The standard pattern

1

Client requests an upload URL

Mutation: generateUploadUrl() returns a one-time URL.
2

Client uploads bytes directly

fetch(url, { method: "POST", body: file }).
3

Client gets back a storage ID

Records the ID in the relevant row via a separate mutation.
4

Server serves the file

Via the storage URL when needed.
See Convex file storage for the code.

Validation

Validate on the client (UX) AND on the server (security):
Don’t trust client-side checks alone. A malicious user can bypass them.

Previews

For images, generate a thumbnail at upload:
Use Cloudinary if you need on-the-fly transforms; see Cloudinary integration.

Multiple files

For multi-file uploads (drag-and-drop a folder), upload in parallel with concurrency control:

Limits

  • Convex max per file: 100 MB by default.
  • Larger files: use AWS S3 integration for multipart uploads.

File uploads recipe

Worked walkthrough.

Convex file storage

Reference.

Cloudinary

Image transformations.
Last modified on April 18, 2026