Convex provides built-in file storage for images, videos, documents, and any other file type. Files are stored securely and can be accessed via temporary URLs.
Storage reader
The StorageReader interface provides read-only access to file storage. Available as ctx.storage in queries, mutations, and actions.
Get file URL
Get a URL to download a file:
import { query } from "./_generated/server";
import { v } from "convex/values";
export const getFileUrl = query({
args: { storageId: v.id("_storage") },
handler: async (ctx, args) => {
const url = await ctx.storage.getUrl(args.storageId);
if (url) {
// Use the URL (e.g., return it to the client)
return url;
}
return null; // File no longer exists
},
});
The ID of the file to fetch from Convex storage.
Returns: A URL which fetches the file via HTTP GET, or null if the file no longer exists.
Note: The GET response includes a standard HTTP Digest header with a SHA-256 checksum for integrity verification.
Get metadata for a stored file via the system table (preferred method):
const metadata = await ctx.db.system.get(storageId);
// metadata: { _id, _creationTime, sha256, size, contentType? }
The metadata document contains:
The storage ID of the file.
Timestamp when the file was uploaded (milliseconds since epoch).
Hex-encoded SHA-256 checksum of file contents.
Size of the file in bytes.
Content type of the file if it was provided on upload (e.g., "image/png"), or null if not specified.
Deprecated: ctx.storage.getMetadata() is deprecated. Use ctx.db.system.get() instead for equivalent metadata.
Storage writer
The StorageWriter interface extends StorageReader with write operations. Available as ctx.storage in mutations.
Generate upload URL
Generate a short-lived URL for uploading a file from the client:
import { mutation } from "./_generated/server";
import { v } from "convex/values";
export const generateUploadUrl = mutation({
args: {},
returns: v.string(),
handler: async (ctx) => {
return await ctx.storage.generateUploadUrl();
},
});
Returns: A short-lived URL for uploading a file via HTTP POST.
The client should POST the file as the request body to this URL:
// On the client:
const uploadUrl = await generateUploadUrl();
const result = await fetch(uploadUrl, {
method: "POST",
headers: { "Content-Type": file.type },
body: file,
});
const { storageId } = await result.json();
// Save storageId to the database
The response is a JSON object containing the newly allocated Id<"_storage">:
{ "storageId": "kg2h4..." }
Delete file
Delete a file from Convex storage:
import { mutation } from "./_generated/server";
import { v } from "convex/values";
export const deleteFile = mutation({
args: { storageId: v.id("_storage") },
handler: async (ctx, args) => {
await ctx.storage.delete(args.storageId);
},
});
The ID of the file to delete from Convex storage.
Once a file is deleted, any URLs previously generated by getUrl() will return 404 errors.
Storage action writer
The StorageActionWriter interface extends StorageWriter with additional methods only available in actions and HTTP actions (not in queries or mutations).
Download file
Download a file from storage as a Blob in an action:
import { action } from "./_generated/server";
import { v } from "convex/values";
export const processImage = action({
args: { storageId: v.id("_storage") },
handler: async (ctx, args) => {
const blob = await ctx.storage.get(args.storageId);
if (!blob) {
throw new Error("File not found");
}
// Process the blob (e.g., resize image, extract metadata)
const arrayBuffer = await blob.arrayBuffer();
// ...
return { size: blob.size, type: blob.type };
},
});
The ID of the file to download.
Returns: A Blob containing the file contents, or null if the file doesn’t exist.
Note: This method is only available in actions and HTTP actions, not in queries or mutations.
Upload file directly
Upload a Blob directly to storage from an action:
import { action } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values";
export const downloadAndStore = action({
args: { url: v.string() },
handler: async (ctx, args) => {
// Fetch file from external URL
const response = await fetch(args.url);
const blob = await response.blob();
// Store in Convex storage
const storageId = await ctx.storage.store(blob);
// Save metadata to database via mutation
await ctx.runMutation(internal.files.saveMetadata, {
storageId,
originalUrl: args.url,
});
return storageId;
},
});
The Blob to store in Convex storage.
Optional settings. Pass sha256 (hex-encoded) to verify file integrity during upload.
Returns: The Id<"_storage"> of the newly stored file.
Note: This method is only available in actions and HTTP actions. For client-side uploads from mutations, use generateUploadUrl() instead.
File upload flow
The typical file upload flow involves three steps:
1. Generate upload URL (mutation)
export const generateUploadUrl = mutation({
args: {},
handler: async (ctx) => {
return await ctx.storage.generateUploadUrl();
},
});
2. Upload file (client)
const uploadUrl = await convex.mutation(api.files.generateUploadUrl);
const result = await fetch(uploadUrl, {
method: "POST",
headers: { "Content-Type": file.type },
body: file,
});
const { storageId } = await result.json();
export const saveFile = mutation({
args: {
storageId: v.id("_storage"),
name: v.string(),
},
handler: async (ctx, args) => {
// Get file metadata
const metadata = await ctx.db.system.get(args.storageId);
// Save to your table
return await ctx.db.insert("files", {
storageId: args.storageId,
name: args.name,
size: metadata?.size,
contentType: metadata?.contentType,
});
},
});
Common patterns
import { mutation } from "./_generated/server";
import { v } from "convex/values";
export const saveImage = mutation({
args: {
storageId: v.id("_storage"),
title: v.string(),
description: v.optional(v.string()),
},
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new Error("Not authenticated");
}
// Get file metadata
const fileMetadata = await ctx.db.system.get(args.storageId);
if (!fileMetadata) {
throw new Error("File not found");
}
// Verify it's an image
if (!fileMetadata.contentType?.startsWith("image/")) {
throw new Error("File must be an image");
}
return await ctx.db.insert("images", {
storageId: args.storageId,
title: args.title,
description: args.description,
uploadedBy: identity.tokenIdentifier,
contentType: fileMetadata.contentType,
size: fileMetadata.size,
});
},
});
export const deleteImage = mutation({
args: { imageId: v.id("images") },
handler: async (ctx, args) => {
const image = await ctx.db.get(args.imageId);
if (!image) {
throw new Error("Image not found");
}
// Delete from storage
await ctx.storage.delete(image.storageId);
// Delete metadata
await ctx.db.delete(args.imageId);
},
});
Process files in actions
import { action } from "./_generated/server";
import { internal } from "./_generated/api";
export const generateThumbnail = action({
args: { storageId: v.id("_storage") },
handler: async (ctx, args) => {
// Download original image
const blob = await ctx.storage.get(args.storageId);
if (!blob) throw new Error("File not found");
// Process image (pseudo-code - use actual image library)
const thumbnail = await resizeImage(blob, { width: 200, height: 200 });
// Upload thumbnail
const thumbnailId = await ctx.storage.store(thumbnail);
// Update database
await ctx.runMutation(internal.images.saveThumbnail, {
originalId: args.storageId,
thumbnailId,
});
return thumbnailId;
},
});
Best practices
- Use
ctx.db.system.get() for metadata - Prefer querying the _storage system table over the deprecated getMetadata() method.
- Verify file types - Check the
contentType to ensure uploaded files match expected types.
- Delete files when documents are deleted - Clean up storage when deleting database records that reference files.
- Use actions for processing - Download and process large files in actions to avoid blocking mutations.
- Generate short-lived URLs - Upload URLs expire, so generate them close to when they’ll be used.
- Store minimal metadata - The
_storage system table already has size and content type; only store additional app-specific metadata.