Claude Agent Skill · by Waynesutton

Convex File Storage

Install Convex File Storage skill for Claude Code from waynesutton/convexskills.

convexfile-storageuploadsimagesfiles
Install
Terminal · npx
$npx skills add https://github.com/waynesutton/convexskills --skill convex-file-storage
Works with Paperclip

How Convex File Storage fits into a Paperclip company.

Convex File Storage drops into any Paperclip agent that handles convex and file-storage work. Assign it to a specialist inside a pre-configured PaperclipOrg company and the skill becomes available on every heartbeat — no prompt engineering, no tool wiring.

S
SaaS FactoryPaired

Pre-configured AI company — 18 agents, 18 skills, one-time purchase.

$27$59
Explore pack
Source file
SKILL.md467 lines
Expand
---name: convex-file-storagedisplayName: Convex File Storagedescription: Complete file handling including upload flows, serving files via URL, storing generated files from actions, deletion, and accessing file metadata from system tablesversion: 1.0.0author: Convextags: [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: - Primary: https://docs.convex.dev/file-storage- Upload Files: https://docs.convex.dev/file-storage/upload-files- Serve Files: https://docs.convex.dev/file-storage/serve-files- For broader context: https://docs.convex.dev/llms.txt ## 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 ```typescript// convex/files.tsimport { 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 ```typescript// React componentimport { 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 {      // Step 1: Get upload URL      const uploadUrl = await generateUploadUrl();       // Step 2: Upload file to storage      const result = await fetch(uploadUrl, {        method: "POST",        headers: { "Content-Type": file.type },        body: file,      });       const { storageId } = await result.json();       // Step 3: Save file reference to database      await saveFile({        storageId,        fileName: file.name,        fileType: file.type,        fileSize: file.size,      });    } finally {      setUploading(false);    }  };   return (    <div>      <input        type="file"        onChange={handleUpload}        disabled={uploading}      />      {uploading && <p>Uploading...</p>}    </div>  );}``` ### Saving File References ```typescript// convex/files.tsimport { 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 ```typescript// convex/files.tsexport 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);  },}); // Get file with URLexport 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);    if (!file) return null;     const url = await ctx.storage.getUrl(file.storageId);        return {      _id: file._id,      fileName: file.fileName,      fileType: file.fileType,      fileSize: file.fileSize,      url,    };  },});``` ### Displaying Files in React ```typescriptimport { 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>;   // Handle different file types  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="600px"      />    );  }   return (    <a href={file.url} download={file.fileName}>      Download {file.fileName}    </a>  );}``` ### Storing Generated Files from Actions ```typescript// convex/generate.ts"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) => {    // Generate PDF (example using a library)    const pdfBuffer = await generatePDFFromContent(args.content);     // Convert to Blob    const blob = new Blob([pdfBuffer], { type: "application/pdf" });     // Store in Convex    const storageId = await ctx.storage.store(blob);     return storageId;  },}); // Generate and save imageexport const generateImage = action({  args: { prompt: v.string() },  returns: v.id("_storage"),  handler: async (ctx, args) => {    // Call external API to generate image    const response = await fetch("https://api.example.com/generate", {      method: "POST",      body: JSON.stringify({ prompt: args.prompt }),    });     const imageBuffer = await response.arrayBuffer();    const blob = new Blob([imageBuffer], { type: "image/png" });     return await ctx.storage.store(blob);  },});``` ### Accessing File Metadata ```typescript// convex/files.tsimport { 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.db.system.get(args.storageId);    return metadata as FileMetadata | null;  },});``` ### Deleting Files ```typescript// convex/files.tsimport { 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;     // Delete from storage    await ctx.storage.delete(file.storageId);     // Delete database record    await ctx.db.delete(args.fileId);     return null;  },});``` ### Image Upload with Preview ```typescriptimport { 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;     // Validate file type    if (!file.type.startsWith("image/")) {      alert("Please select an image file");      return;    }     // Validate file size (max 10MB)    if (file.size > 10 * 1024 * 1024) {      alert("File size must be less than 10MB");      return;    }     // Show preview    const reader = new FileReader();    reader.onload = (e) => setPreview(e.target?.result as string);    reader.readAsDataURL(file);     // Upload    setUploading(true);    try {      const uploadUrl = await generateUploadUrl();      const result = await fetch(uploadUrl, {        method: "POST",        headers: { "Content-Type": file.type },        body: file,      });       const { storageId } = await result.json();      const fileId = await saveFile({        storageId,        fileName: file.name,        fileType: file.type,        fileSize: file.size,      });       onUpload(fileId);    } finally {      setUploading(false);    }  };   return (    <div>      <input        ref={inputRef}        type="file"        accept="image/*"        onChange={handleFileSelect}        style={{ display: "none" }}      />            <button        onClick={() => inputRef.current?.click()}        disabled={uploading}      >        {uploading ? "Uploading..." : "Select Image"}      </button>       {preview && (        <img          src={preview}          alt="Preview"          style={{ maxWidth: 200, marginTop: 10 }}        />      )}    </div>  );}``` ## Examples ### Schema for File Storage ```typescript// convex/schema.tsimport { 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"]),   // User avatars  users: defineTable({    name: v.string(),    email: v.string(),    avatarStorageId: v.optional(v.id("_storage")),  }),   // Posts with images  posts: defineTable({    authorId: v.id("users"),    content: v.string(),    imageStorageIds: v.array(v.id("_storage")),    createdAt: v.number(),  }).index("by_author", ["authorId"]),});``` ## Best Practices - Never run `npx 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 1. **Not setting Content-Type header** - Files may not serve correctly2. **Forgetting to delete storage** - Orphaned files waste storage3. **Not validating file types** - Security risk for malicious uploads4. **Large file uploads without progress** - Poor UX for users5. **Using deprecated getMetadata** - Use ctx.db.system.get instead ## References - Convex Documentation: https://docs.convex.dev/- Convex LLMs.txt: https://docs.convex.dev/llms.txt- File Storage: https://docs.convex.dev/file-storage- Upload Files: https://docs.convex.dev/file-storage/upload-files- Serve Files: https://docs.convex.dev/file-storage/serve-files