File Storage
Convex includes a built-in file storage system for uploading, storing, and serving files. It handles the infrastructure so you do not need a separate object store or CDN.
Upload flow
The upload process has three steps:
- Generate an upload URL — call a mutation that returns a short-lived upload URL.
- POST the file — send the file to the upload URL from the client.
- Save the storage ID — store the returned
storageIdin a document for later retrieval.
Server functions
// convex/files.ts
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
export const generateUploadUrl = mutation(async (ctx) => {
return await ctx.storage.generateUploadUrl();
});
export const saveFile = mutation({
args: { storageId: v.id("_storage"), name: v.string() },
handler: async (ctx, args) => {
await ctx.db.insert("files", {
storageId: args.storageId,
name: args.name,
uploadedAt: Date.now(),
});
},
});
export const getFileUrl = query({
args: { storageId: v.id("_storage") },
handler: async (ctx, args) => {
return await ctx.storage.getUrl(args.storageId);
},
});
Client component
"use client";
import { useMutation } from "convex/react";
import { api } from "@/convex/_generated/api";
export function FileUploader() {
const generateUrl = useMutation(api.files.generateUploadUrl);
const saveFile = useMutation(api.files.saveFile);
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
const uploadUrl = await generateUrl();
const result = await fetch(uploadUrl, {
method: "POST",
headers: { "Content-Type": file.type },
body: file,
});
const { storageId } = await result.json();
await saveFile({ storageId, name: file.name });
};
return <input type="file" onChange={handleUpload} />;
}
Serving files
Use ctx.storage.getUrl(storageId) inside a query to generate a public URL for any stored file.
The URL is stable and CDN-backed.
Storage API reference
| Method | Description |
|---|---|
ctx.storage.generateUploadUrl() | Create a temporary upload URL |
ctx.storage.getUrl(storageId) | Get the public serving URL |
ctx.storage.delete(storageId) | Remove a file from storage |
ctx.storage.getMetadata(storageId) | Retrieve file size and content type |
Tips
- Store the
storageIdin a document field typed asv.id("_storage"). - Set
Content-Typeon the upload request so Convex serves files with the correct MIME type. - Clean up orphaned files by deleting the storage entry when the parent document is removed.