Skip to main content
Convex includes managed file storage. Upload via mutation, get back a storage ID, serve via Convex’s CDN. No S3, R2, or Cloudflare setup required.

How it works

1

Generate an upload URL

A Convex mutation calls ctx.storage.generateUploadUrl() and returns a one-time URL.
2

Client uploads directly

await fetch(uploadUrl, { method: "POST", body: file }) — bytes go straight to Convex storage, not through your function.
3

Save the storage ID

Mutation stores storageId on a row in your schema (e.g., users.avatarStorageId).
4

Serve via URL

Read with ctx.storage.getUrl(storageId) — returns a CDN URL good for serving.

Example

convex/files.ts
Client:

Limits

For files larger than 100 MB, use a multi-part upload pattern or store in S3 / R2 via the AWS S3 integration.

Image transformations

Append query params to the URL for on-the-fly transformations:
For more advanced needs (multiple breakpoints, HEIC conversion), use Cloudinary.

Tips

Always validate file type and size on the server. Client-side checks are user-friendly but bypassable.
Store file metadata separately. Filename, MIME type, original dimensions — store these alongside the storage ID for easier display.

File uploads recipe

Complete walkthrough.

Cloudinary

For advanced image transformations.

Convex actions

For uploads that need to call third-party APIs.
Last modified on April 18, 2026