Install any skill in seconds. Free to start, no credit card required.
Get Started Free →waynesutton/convex-file-storage
Complete file handling including upload flows, serving files via URL, storing generated files from actions, deletion, and accessing file metadata from system tables
Passed 23 of 23 test cases with the skill, and 22 of 23 without it.
1 fixed22 passed either way0 still failing0 broke
Tokens, turns and time describe resource use, not a dollar price.
This benchmark measured an earlier version of the skill.
A newer attempt on gemini-3.6-flash on 10/3/2026 did not produce a publishable comparison (not comparable). The snapshot above is the previously measured run.
A test case the skill fixed. Same model, same prompt, with and without the skill.
A developer asks whether to use ctx.storage.getMetadata(storageId) or ctx.db.system.get(storageId) to look up stored file details in Convex. Which approach is recommended in modern Convex development and why?
| Checked | Without skill | With skill |
|---|---|---|
| Use ctx.db.system.get because fetching directly from the system table replaces deprecated metadata helpers. | Missed | Passed |
The instructions your model reads when the skill loads.
Produces the upload, store, serve, and delete functions for files in Convex. The one rule: persist the Id<"_storage">, never the URL string. URLs are resolved in queries with ctx.storage.getUrl at read time.
Keep your own table for app metadata (name, owner, purpose) and point at _storage by id. Convex keeps size, content type, and hash in the _storage system table.
// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
files: defineTable({
storageId: v.id("_storage"),
name: v.string(),
contentType: v.string(),
size: v.number(),
ownerId: v.id("users"),
}).index("by_owner", ["ownerId"]),
});
Three steps: a mutation hands out a short lived upload URL, the client POSTs the file to it, then a second mutation saves the returned storageId. Upload URLs expire after one hour, so generate a fresh one per upload and never store it.
// convex/files.ts
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
import { getCurrentUser } from "./lib/auth";
const MAX_BYTES = 10 * 1024 * 1024;
const ALLOWED_TYPES = ["image/png", "image/jpeg", "image/webp", "application/pdf"];
export const generateUploadUrl = mutation({
args: {},
returns: v.string(),
test cases passed, vs 22 / 23 without
More skills from the same source and nearby in the registry, with their headline benchmark lift.