| name | convex-file-storage |
| displayName | Convex File Storage |
| description | Complete file handling including upload flows, serving files via URL, storing generated files from actions, deletion, and accessing file metadata from system tables |
| version | 1.0.0 |
| author | Convex |
| tags | ["convex","file-storage","uploads","images","files"] |
Convex File Storage
Handle file uploads, storage, serving, and management in Convex applications with proper patterns for images, documents, and generated files.
Documentation Sources
Before implementing, do not assume; fetch the latest documentation:
Instructions
File Storage Overview
Convex provides built-in file storage with:
- Automatic URL generation for serving files
- Support for any file type (images, PDFs, videos, etc.)
- File metadata via the
_storage system table
- Integration with mutations and actions
Generating Upload URLs
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();
},
});
Client-Side Upload
import { useMutation } from "convex/react";
import { api } from "../convex/_generated/api";
import { useState } from "react";
function FileUploader() {
const generateUploadUrl = useMutation(api.files.generateUploadUrl);
const saveFile = useMutation(api.files.saveFile);
const [uploading, setUploading] = useState(false);
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setUploading(true);
try {
const uploadUrl = await generateUploadUrl();
const result = await fetch(uploadUrl, {
method: ,
: { : file. },
: file,
});
{ storageId } = result.();
({
storageId,
: file.,
: file.,
: file.,
});
} {
();
}
};
(
);
}
Saving File References
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
export const saveFile = mutation({
args: {
storageId: v.id("_storage"),
fileName: v.string(),
fileType: v.string(),
fileSize: v.number(),
},
returns: v.id("files"),
handler: async (ctx, args) => {
return await ctx.db.insert("files", {
storageId: args.storageId,
fileName: args.fileName,
fileType: args.fileType,
fileSize: args.fileSize,
uploadedAt: Date.now(),
});
},
});
Serving Files via URL
export const getFileUrl = query({
args: { storageId: v.id("_storage") },
returns: v.union(v.string(), v.null()),
handler: async (ctx, args) => {
return await ctx.storage.getUrl(args.storageId);
},
});
export const getFile = query({
args: { fileId: v.id("files") },
returns: v.union(
v.object({
_id: v.id("files"),
fileName: v.string(),
fileType: v.string(),
fileSize: v.number(),
url: v.union(v.string(), v.null()),
}),
v.null()
),
handler: async (ctx, args) => {
const file = await ctx.db.get(args.fileId);
(!file) ;
url = ctx..(file.);
{
: file.,
: file.,
: file.,
: file.,
url,
};
},
});
Displaying Files in React
import { useQuery } from "convex/react";
import { api } from "../convex/_generated/api";
function FileDisplay({ fileId }: { fileId: Id<"files"> }) {
const file = useQuery(api.files.getFile, { fileId });
if (!file) return <div>Loading...</div>;
if (!file.url) return <div>File not found</div>;
if (file.fileType.startsWith("image/")) {
return <img src={file.url} alt={file.fileName} />;
}
if (file.fileType === "application/pdf") {
return (
<iframe
src={file.url}
title={file.fileName}
width="100%"
height=
/>
);
}
(
);
}
Storing Generated Files from Actions
"use node";
import { action } from "./_generated/server";
import { v } from "convex/values";
import { api } from "./_generated/api";
export const generatePDF = action({
args: { content: v.string() },
returns: v.id("_storage"),
handler: async (ctx, args) => {
const pdfBuffer = await generatePDFFromContent(args.content);
const blob = new Blob([pdfBuffer], { type: "application/pdf" });
const storageId = await ctx.storage.store(blob);
return storageId;
},
});
export const generateImage = action({
args: { prompt: v.string() },
returns: v.id("_storage"),
handler: (ctx, args) => {
response = (, {
: ,
: .({ : args. }),
});
imageBuffer = response.();
blob = ([imageBuffer], { : });
ctx..(blob);
},
});
Accessing File Metadata
import { query } from "./_generated/server";
import { v } from "convex/values";
import { Id } from "./_generated/dataModel";
type FileMetadata = {
_id: Id<"_storage">;
_creationTime: number;
contentType?: string;
sha256: string;
size: number;
};
export const getFileMetadata = query({
args: { storageId: v.id("_storage") },
returns: v.union(
v.object({
_id: v.id("_storage"),
_creationTime: v.number(),
contentType: v.optional(v.string()),
sha256: v.string(),
size: v.number(),
}),
v.null()
),
handler: async (ctx, args) => {
const metadata = await ctx...(args.);
metadata | ;
},
});
Deleting Files
import { mutation } from "./_generated/server";
import { v } from "convex/values";
export const deleteFile = mutation({
args: { fileId: v.id("files") },
returns: v.null(),
handler: async (ctx, args) => {
const file = await ctx.db.get(args.fileId);
if (!file) return null;
await ctx.storage.delete(file.storageId);
await ctx.db.delete(args.fileId);
return null;
},
});
Image Upload with Preview
import { useMutation } from "convex/react";
import { api } from "../convex/_generated/api";
import { useState, useRef } from "react";
function ImageUploader({ onUpload }: { onUpload: (id: Id<"files">) => void }) {
const generateUploadUrl = useMutation(api.files.generateUploadUrl);
const saveFile = useMutation(api.files.saveFile);
const [preview, setPreview] = useState<string | null>(null);
const [uploading, setUploading] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
if (!file.type.startsWith()) {
();
;
}
(file. > * * ) {
();
;
}
reader = ();
reader. = (e.?. );
reader.(file);
();
{
uploadUrl = ();
result = (uploadUrl, {
: ,
: { : file. },
: file,
});
{ storageId } = result.();
fileId = ({
storageId,
: file.,
: file.,
: file.,
});
(fileId);
} {
();
}
};
(
);
}
Examples
Schema for File Storage
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
files: defineTable({
storageId: v.id("_storage"),
fileName: v.string(),
fileType: v.string(),
fileSize: v.number(),
uploadedBy: v.id("users"),
uploadedAt: v.number(),
})
.index("by_user", ["uploadedBy"])
.index("by_type", ["fileType"]),
users: defineTable({
name: v.string(),
email: v.string(),
avatarStorageId: v.optional(v.id("_storage")),
}),
posts: defineTable({
authorId: v.id("users"),
content: v.string(),
: v.(v.()),
: v.(),
}).(, []),
});
Best Practices
- Never run
bunx convex deploy unless explicitly instructed
- Never run any git commands unless explicitly instructed
- Validate file types and sizes on the client before uploading
- Store file metadata (name, type, size) in your own table
- Use the
_storage system table only for Convex metadata
- Delete storage files when deleting database references
- Use appropriate Content-Type headers when uploading
- Consider image optimization for large images
Common Pitfalls
- Not setting Content-Type header - Files may not serve correctly
- Forgetting to delete storage - Orphaned files waste storage
- Not validating file types - Security risk for malicious uploads
- Large file uploads without progress - Poor UX for users
- Using deprecated getMetadata - Use ctx.db.system.get instead
References