skills/convex-file-storage/SKILL.md
File uploads, storage, and serving in Convex. Use when implementing file uploads, generating upload URLs, serving files, managing file metadata, or building file-based features like avatars, attachments, or media galleries.
npx skillsauth add aaronvanston/skills-convex convex-file-storageInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
// convex/files.ts
import { mutation, query } 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
async function uploadFile(file: File) {
// Get upload URL from Convex
const uploadUrl = await generateUploadUrl();
// Upload file directly to Convex storage
const response = await fetch(uploadUrl, {
method: "POST",
headers: { "Content-Type": file.type },
body: file,
});
const { storageId } = await response.json();
return storageId;
}
export const saveFile = mutation({
args: {
storageId: v.id("_storage"),
fileName: v.string(),
fileType: v.string(),
},
returns: v.id("files"),
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new ConvexError({ code: "UNAUTHENTICATED", message: "Not logged in" });
}
return await ctx.db.insert("files", {
storageId: args.storageId,
fileName: args.fileName,
fileType: args.fileType,
uploadedBy: identity.subject,
uploadedAt: Date.now(),
});
},
});
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"),
url: v.union(v.string(), v.null()),
fileName: v.string(),
fileType: v.string(),
}),
v.null()
),
handler: async (ctx, args) => {
const file = await ctx.db.get(args.fileId);
if (!file) return null;
const url = await ctx.storage.getUrl(file.storageId);
return {
_id: file._id,
url,
fileName: file.fileName,
fileType: file.fileType,
};
},
});
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) {
throw new ConvexError({ code: "NOT_FOUND", message: "File not found" });
}
// Delete from storage
await ctx.storage.delete(file.storageId);
// Delete metadata
await ctx.db.delete(args.fileId);
return null;
},
});
// convex/schema.ts
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.optional(v.number()),
uploadedBy: v.string(),
uploadedAt: v.number(),
})
.index("by_uploader", ["uploadedBy"])
.index("by_type", ["fileType"]),
});
export const saveImage = mutation({
args: {
storageId: v.id("_storage"),
width: v.number(),
height: v.number(),
},
returns: v.id("images"),
handler: async (ctx, args) => {
return await ctx.db.insert("images", {
storageId: args.storageId,
width: args.width,
height: args.height,
createdAt: Date.now(),
});
},
});
// React component example
function ImageUpload({ onUpload }: { onUpload: (id: string) => void }) {
const generateUploadUrl = useMutation(api.files.generateUploadUrl);
const saveImage = useMutation(api.files.saveImage);
const [preview, setPreview] = useState<string | null>(null);
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
// Show preview
setPreview(URL.createObjectURL(file));
// Get dimensions
const img = new Image();
img.src = URL.createObjectURL(file);
await new Promise((resolve) => (img.onload = resolve));
// Upload
const uploadUrl = await generateUploadUrl();
const response = await fetch(uploadUrl, {
method: "POST",
headers: { "Content-Type": file.type },
body: file,
});
const { storageId } = await response.json();
// Save with dimensions
const imageId = await saveImage({
storageId,
width: img.naturalWidth,
height: img.naturalHeight,
});
onUpload(imageId);
};
return (
<div>
<input type="file" accept="image/*" onChange={handleFileChange} />
{preview && <img src={preview} alt="Preview" />}
</div>
);
}
// convex/http.ts
import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";
const http = httpRouter();
http.route({
path: "/files/{storageId}",
method: "GET",
handler: httpAction(async (ctx, request) => {
const url = new URL(request.url);
const storageId = url.pathname.split("/").pop();
if (!storageId) {
return new Response("Missing storageId", { status: 400 });
}
const blob = await ctx.storage.get(storageId as Id<"_storage">);
if (!blob) {
return new Response("File not found", { status: 404 });
}
return new Response(blob);
}),
});
export default http;
tools
Security best practices for Convex functions including ConvexError handling, argument/return validation, authentication helpers, access control, rate limiting, and internal functions. Use when writing public queries/mutations/actions, implementing authentication, adding authorization checks, handling errors, or reviewing Convex functions for security.
development
Comprehensive Convex code review checklist for production readiness. Use when auditing a Convex codebase before deployment, reviewing pull requests, or checking for security and performance issues in Convex functions.
data-ai
Realtime subscriptions and optimistic updates in Convex. Use when implementing live data updates, optimistic UI, pagination with realtime, presence indicators, typing indicators, or any feature requiring instant data synchronization.
data-ai
Best practices for Convex database queries, indexes, and filtering. Use when writing or reviewing database queries in Convex, working with `.filter()`, `.collect()`, `.withIndex()`, defining indexes in schema.ts, or optimizing query performance.