[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6CCCTKCJ09KT691Q1AGVJK7":3,"skill-same:01M6CCCTKCJ09KT691Q1AGVJK7":61},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":31,"linkOnly":32,"markdownContent":33,"markdownBody":34,"frontMatter":35,"refName":40,"commitSha":41,"skillDirectory":42,"forksCount":14,"repositoryHtmlUrl":43,"repositoryHomepageUrl":44,"ownerHtmlUrl":45,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":46,"install":47,"resources":60},"01M6CCCTKCJ09KT691Q1AGVJK7","convex-file-storage",null,"Complete file handling including upload flows, serving files via URL, storing generated files from actions, deletion, and accessing file metadata from system tables","baisharg\u002FASTN","baisharg","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F204916775?v=4","01M6CC8XHZYVK2DR22FZ1BESCG","BAISH","01M6CC8XP400QZ9DXAWTC8EG3X",2,"AGPL-3.0","https:\u002F\u002Fgithub.com\u002Fbaisharg\u002FASTN\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fconvex-file-storage\u002FSKILL.md",[],[19,20,21,22,23,24,25,26,27,28,29,30],"convex","storage","file","convex file storage","metadata","actions","system","generated","images","files","file-storage","uploads","2026-11-10 10:12:30",false,"---\nname: convex-file-storage\ndisplayName: Convex File Storage\ndescription: Complete file handling including upload flows, serving files via URL, storing generated files from actions, deletion, and accessing file metadata from system tables\nversion: 1.0.0\nauthor: Convex\ntags: [convex, file-storage, uploads, images, files]\n---\n\n# Convex File Storage\n\nHandle file uploads, storage, serving, and management in Convex applications with proper patterns for images, documents, and generated files.\n\n## Documentation Sources\n\nBefore implementing, do not assume; fetch the latest documentation:\n\n- Primary: https:\u002F\u002Fdocs.convex.dev\u002Ffile-storage\n- Upload Files: https:\u002F\u002Fdocs.convex.dev\u002Ffile-storage\u002Fupload-files\n- Serve Files: https:\u002F\u002Fdocs.convex.dev\u002Ffile-storage\u002Fserve-files\n- For broader context: https:\u002F\u002Fdocs.convex.dev\u002Fllms.txt\n\n## Instructions\n\n### File Storage Overview\n\nConvex provides built-in file storage with:\n\n- Automatic URL generation for serving files\n- Support for any file type (images, PDFs, videos, etc.)\n- File metadata via the `_storage` system table\n- Integration with mutations and actions\n\n### Generating Upload URLs\n\n```typescript\n\u002F\u002F convex\u002Ffiles.ts\nimport { mutation } from '.\u002F_generated\u002Fserver'\nimport { v } from 'convex\u002Fvalues'\n\nexport const generateUploadUrl = mutation({\n  args: {},\n  returns: v.string(),\n  handler: async (ctx) => {\n    return await ctx.storage.generateUploadUrl()\n  },\n})\n```\n\n### Client-Side Upload\n\n```typescript\n\u002F\u002F React component\nimport { useMutation } from \"convex\u002Freact\";\nimport { api } from \"..\u002Fconvex\u002F_generated\u002Fapi\";\nimport { useState } from \"react\";\n\nfunction FileUploader() {\n  const generateUploadUrl = useMutation(api.files.generateUploadUrl);\n  const saveFile = useMutation(api.files.saveFile);\n  const [uploading, setUploading] = useState(false);\n\n  const handleUpload = async (e: React.ChangeEvent\u003CHTMLInputElement>) => {\n    const file = e.target.files?.[0];\n    if (!file) return;\n\n    setUploading(true);\n    try {\n      \u002F\u002F Step 1: Get upload URL\n      const uploadUrl = await generateUploadUrl();\n\n      \u002F\u002F Step 2: Upload file to storage\n      const result = await fetch(uploadUrl, {\n        method: \"POST\",\n        headers: { \"Content-Type\": file.type },\n        body: file,\n      });\n\n      const { storageId } = await result.json();\n\n      \u002F\u002F Step 3: Save file reference to database\n      await saveFile({\n        storageId,\n        fileName: file.name,\n        fileType: file.type,\n        fileSize: file.size,\n      });\n    } finally {\n      setUploading(false);\n    }\n  };\n\n  return (\n    \u003Cdiv>\n      \u003Cinput\n        type=\"file\"\n        onChange={handleUpload}\n        disabled={uploading}\n      \u002F>\n      {uploading && \u003Cp>Uploading...\u003C\u002Fp>}\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### Saving File References\n\n```typescript\n\u002F\u002F convex\u002Ffiles.ts\nimport { mutation, query } from '.\u002F_generated\u002Fserver'\nimport { v } from 'convex\u002Fvalues'\n\nexport const saveFile = mutation({\n  args: {\n    storageId: v.id('_storage'),\n    fileName: v.string(),\n    fileType: v.string(),\n    fileSize: v.number(),\n  },\n  returns: v.id('files'),\n  handler: async (ctx, args) => {\n    return await ctx.db.insert('files', {\n      storageId: args.storageId,\n      fileName: args.fileName,\n      fileType: args.fileType,\n      fileSize: args.fileSize,\n      uploadedAt: Date.now(),\n    })\n  },\n})\n```\n\n### Serving Files via URL\n\n```typescript\n\u002F\u002F convex\u002Ffiles.ts\nexport const getFileUrl = query({\n  args: { storageId: v.id('_storage') },\n  returns: v.union(v.string(), v.null()),\n  handler: async (ctx, args) => {\n    return await ctx.storage.getUrl(args.storageId)\n  },\n})\n\n\u002F\u002F Get file with URL\nexport const getFile = query({\n  args: { fileId: v.id('files') },\n  returns: v.union(\n    v.object({\n      _id: v.id('files'),\n      fileName: v.string(),\n      fileType: v.string(),\n      fileSize: v.number(),\n      url: v.union(v.string(), v.null()),\n    }),\n    v.null(),\n  ),\n  handler: async (ctx, args) => {\n    const file = await ctx.db.get(args.fileId)\n    if (!file) return null\n\n    const url = await ctx.storage.getUrl(file.storageId)\n\n    return {\n      _id: file._id,\n      fileName: file.fileName,\n      fileType: file.fileType,\n      fileSize: file.fileSize,\n      url,\n    }\n  },\n})\n```\n\n### Displaying Files in React\n\n```typescript\nimport { useQuery } from \"convex\u002Freact\";\nimport { api } from \"..\u002Fconvex\u002F_generated\u002Fapi\";\n\nfunction FileDisplay({ fileId }: { fileId: Id\u003C\"files\"> }) {\n  const file = useQuery(api.files.getFile, { fileId });\n\n  if (!file) return \u003Cdiv>Loading...\u003C\u002Fdiv>;\n  if (!file.url) return \u003Cdiv>File not found\u003C\u002Fdiv>;\n\n  \u002F\u002F Handle different file types\n  if (file.fileType.startsWith(\"image\u002F\")) {\n    return \u003Cimg src={file.url} alt={file.fileName} \u002F>;\n  }\n\n  if (file.fileType === \"application\u002Fpdf\") {\n    return (\n      \u003Ciframe\n        src={file.url}\n        title={file.fileName}\n        width=\"100%\"\n        height=\"600px\"\n      \u002F>\n    );\n  }\n\n  return (\n    \u003Ca href={file.url} download={file.fileName}>\n      Download {file.fileName}\n    \u003C\u002Fa>\n  );\n}\n```\n\n### Storing Generated Files from Actions\n\n```typescript\n\u002F\u002F convex\u002Fgenerate.ts\n'use node'\n\nimport { action } from '.\u002F_generated\u002Fserver'\nimport { v } from 'convex\u002Fvalues'\nimport { api } from '.\u002F_generated\u002Fapi'\n\nexport const generatePDF = action({\n  args: { content: v.string() },\n  returns: v.id('_storage'),\n  handler: async (ctx, args) => {\n    \u002F\u002F Generate PDF (example using a library)\n    const pdfBuffer = await generatePDFFromContent(args.content)\n\n    \u002F\u002F Convert to Blob\n    const blob = new Blob([pdfBuffer], { type: 'application\u002Fpdf' })\n\n    \u002F\u002F Store in Convex\n    const storageId = await ctx.storage.store(blob)\n\n    return storageId\n  },\n})\n\n\u002F\u002F Generate and save image\nexport const generateImage = action({\n  args: { prompt: v.string() },\n  returns: v.id('_storage'),\n  handler: async (ctx, args) => {\n    \u002F\u002F Call external API to generate image\n    const response = await fetch('https:\u002F\u002Fapi.example.com\u002Fgenerate', {\n      method: 'POST',\n      body: JSON.stringify({ prompt: args.prompt }),\n    })\n\n    const imageBuffer = await response.arrayBuffer()\n    const blob = new Blob([imageBuffer], { type: 'image\u002Fpng' })\n\n    return await ctx.storage.store(blob)\n  },\n})\n```\n\n### Accessing File Metadata\n\n```typescript\n\u002F\u002F convex\u002Ffiles.ts\nimport { query } from '.\u002F_generated\u002Fserver'\nimport { v } from 'convex\u002Fvalues'\nimport { Id } from '.\u002F_generated\u002FdataModel'\n\ntype FileMetadata = {\n  _id: Id\u003C'_storage'>\n  _creationTime: number\n  contentType?: string\n  sha256: string\n  size: number\n}\n\nexport const getFileMetadata = query({\n  args: { storageId: v.id('_storage') },\n  returns: v.union(\n    v.object({\n      _id: v.id('_storage'),\n      _creationTime: v.number(),\n      contentType: v.optional(v.string()),\n      sha256: v.string(),\n      size: v.number(),\n    }),\n    v.null(),\n  ),\n  handler: async (ctx, args) => {\n    const metadata = await ctx.db.system.get(args.storageId)\n    return metadata as FileMetadata | null\n  },\n})\n```\n\n### Deleting Files\n\n```typescript\n\u002F\u002F convex\u002Ffiles.ts\nimport { mutation } from '.\u002F_generated\u002Fserver'\nimport { v } from 'convex\u002Fvalues'\n\nexport const deleteFile = mutation({\n  args: { fileId: v.id('files') },\n  returns: v.null(),\n  handler: async (ctx, args) => {\n    const file = await ctx.db.get(args.fileId)\n    if (!file) return null\n\n    \u002F\u002F Delete from storage\n    await ctx.storage.delete(file.storageId)\n\n    \u002F\u002F Delete database record\n    await ctx.db.delete(args.fileId)\n\n    return null\n  },\n})\n```\n\n### Image Upload with Preview\n\n```typescript\nimport { useMutation } from \"convex\u002Freact\";\nimport { api } from \"..\u002Fconvex\u002F_generated\u002Fapi\";\nimport { useState, useRef } from \"react\";\n\nfunction ImageUploader({ onUpload }: { onUpload: (id: Id\u003C\"files\">) => void }) {\n  const generateUploadUrl = useMutation(api.files.generateUploadUrl);\n  const saveFile = useMutation(api.files.saveFile);\n  const [preview, setPreview] = useState\u003Cstring | null>(null);\n  const [uploading, setUploading] = useState(false);\n  const inputRef = useRef\u003CHTMLInputElement>(null);\n\n  const handleFileSelect = async (e: React.ChangeEvent\u003CHTMLInputElement>) => {\n    const file = e.target.files?.[0];\n    if (!file) return;\n\n    \u002F\u002F Validate file type\n    if (!file.type.startsWith(\"image\u002F\")) {\n      alert(\"Please select an image file\");\n      return;\n    }\n\n    \u002F\u002F Validate file size (max 10MB)\n    if (file.size > 10 * 1024 * 1024) {\n      alert(\"File size must be less than 10MB\");\n      return;\n    }\n\n    \u002F\u002F Show preview\n    const reader = new FileReader();\n    reader.onload = (e) => setPreview(e.target?.result as string);\n    reader.readAsDataURL(file);\n\n    \u002F\u002F Upload\n    setUploading(true);\n    try {\n      const uploadUrl = await generateUploadUrl();\n      const result = await fetch(uploadUrl, {\n        method: \"POST\",\n        headers: { \"Content-Type\": file.type },\n        body: file,\n      });\n\n      const { storageId } = await result.json();\n      const fileId = await saveFile({\n        storageId,\n        fileName: file.name,\n        fileType: file.type,\n        fileSize: file.size,\n      });\n\n      onUpload(fileId);\n    } finally {\n      setUploading(false);\n    }\n  };\n\n  return (\n    \u003Cdiv>\n      \u003Cinput\n        ref={inputRef}\n        type=\"file\"\n        accept=\"image\u002F*\"\n        onChange={handleFileSelect}\n        style={{ display: \"none\" }}\n      \u002F>\n\n      \u003Cbutton\n        onClick={() => inputRef.current?.click()}\n        disabled={uploading}\n      >\n        {uploading ? \"Uploading...\" : \"Select Image\"}\n      \u003C\u002Fbutton>\n\n      {preview && (\n        \u003Cimg\n          src={preview}\n          alt=\"Preview\"\n          style={{ maxWidth: 200, marginTop: 10 }}\n        \u002F>\n      )}\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n## Examples\n\n### Schema for File Storage\n\n```typescript\n\u002F\u002F convex\u002Fschema.ts\nimport { defineSchema, defineTable } from 'convex\u002Fserver'\nimport { v } from 'convex\u002Fvalues'\n\nexport default defineSchema({\n  files: defineTable({\n    storageId: v.id('_storage'),\n    fileName: v.string(),\n    fileType: v.string(),\n    fileSize: v.number(),\n    uploadedBy: v.id('users'),\n    uploadedAt: v.number(),\n  })\n    .index('by_user', ['uploadedBy'])\n    .index('by_type', ['fileType']),\n\n  \u002F\u002F User avatars\n  users: defineTable({\n    name: v.string(),\n    email: v.string(),\n    avatarStorageId: v.optional(v.id('_storage')),\n  }),\n\n  \u002F\u002F Posts with images\n  posts: defineTable({\n    authorId: v.id('users'),\n    content: v.string(),\n    imageStorageIds: v.array(v.id('_storage')),\n    createdAt: v.number(),\n  }).index('by_author', ['authorId']),\n})\n```\n\n## Best Practices\n\n- Never run `npx convex deploy` unless explicitly instructed\n- Never run any git commands unless explicitly instructed\n- Validate file types and sizes on the client before uploading\n- Store file metadata (name, type, size) in your own table\n- Use the `_storage` system table only for Convex metadata\n- Delete storage files when deleting database references\n- Use appropriate Content-Type headers when uploading\n- Consider image optimization for large images\n\n## Common Pitfalls\n\n1. **Not setting Content-Type header** - Files may not serve correctly\n2. **Forgetting to delete storage** - Orphaned files waste storage\n3. **Not validating file types** - Security risk for malicious uploads\n4. **Large file uploads without progress** - Poor UX for users\n5. **Using deprecated getMetadata** - Use ctx.db.system.get instead\n\n## References\n\n- Convex Documentation: https:\u002F\u002Fdocs.convex.dev\u002F\n- Convex LLMs.txt: https:\u002F\u002Fdocs.convex.dev\u002Fllms.txt\n- File Storage: https:\u002F\u002Fdocs.convex.dev\u002Ffile-storage\n- Upload Files: https:\u002F\u002Fdocs.convex.dev\u002Ffile-storage\u002Fupload-files\n- Serve Files: https:\u002F\u002Fdocs.convex.dev\u002Ffile-storage\u002Fserve-files\n","# Convex File Storage\n\nHandle file uploads, storage, serving, and management in Convex applications with proper patterns for images, documents, and generated files.\n\n## Documentation Sources\n\nBefore implementing, do not assume; fetch the latest documentation:\n\n- Primary: https:\u002F\u002Fdocs.convex.dev\u002Ffile-storage\n- Upload Files: https:\u002F\u002Fdocs.convex.dev\u002Ffile-storage\u002Fupload-files\n- Serve Files: https:\u002F\u002Fdocs.convex.dev\u002Ffile-storage\u002Fserve-files\n- For broader context: https:\u002F\u002Fdocs.convex.dev\u002Fllms.txt\n\n## Instructions\n\n### File Storage Overview\n\nConvex provides built-in file storage with:\n\n- Automatic URL generation for serving files\n- Support for any file type (images, PDFs, videos, etc.)\n- File metadata via the `_storage` system table\n- Integration with mutations and actions\n\n### Generating Upload URLs\n\n```typescript\n\u002F\u002F convex\u002Ffiles.ts\nimport { mutation } from '.\u002F_generated\u002Fserver'\nimport { v } from 'convex\u002Fvalues'\n\nexport const generateUploadUrl = mutation({\n  args: {},\n  returns: v.string(),\n  handler: async (ctx) => {\n    return await ctx.storage.generateUploadUrl()\n  },\n})\n```\n\n### Client-Side Upload\n\n```typescript\n\u002F\u002F React component\nimport { useMutation } from \"convex\u002Freact\";\nimport { api } from \"..\u002Fconvex\u002F_generated\u002Fapi\";\nimport { useState } from \"react\";\n\nfunction FileUploader() {\n  const generateUploadUrl = useMutation(api.files.generateUploadUrl);\n  const saveFile = useMutation(api.files.saveFile);\n  const [uploading, setUploading] = useState(false);\n\n  const handleUpload = async (e: React.ChangeEvent\u003CHTMLInputElement>) => {\n    const file = e.target.files?.[0];\n    if (!file) return;\n\n    setUploading(true);\n    try {\n      \u002F\u002F Step 1: Get upload URL\n      const uploadUrl = await generateUploadUrl();\n\n      \u002F\u002F Step 2: Upload file to storage\n      const result = await fetch(uploadUrl, {\n        method: \"POST\",\n        headers: { \"Content-Type\": file.type },\n        body: file,\n      });\n\n      const { storageId } = await result.json();\n\n      \u002F\u002F Step 3: Save file reference to database\n      await saveFile({\n        storageId,\n        fileName: file.name,\n        fileType: file.type,\n        fileSize: file.size,\n      });\n    } finally {\n      setUploading(false);\n    }\n  };\n\n  return (\n    \u003Cdiv>\n      \u003Cinput\n        type=\"file\"\n        onChange={handleUpload}\n        disabled={uploading}\n      \u002F>\n      {uploading && \u003Cp>Uploading...\u003C\u002Fp>}\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### Saving File References\n\n```typescript\n\u002F\u002F convex\u002Ffiles.ts\nimport { mutation, query } from '.\u002F_generated\u002Fserver'\nimport { v } from 'convex\u002Fvalues'\n\nexport const saveFile = mutation({\n  args: {\n    storageId: v.id('_storage'),\n    fileName: v.string(),\n    fileType: v.string(),\n    fileSize: v.number(),\n  },\n  returns: v.id('files'),\n  handler: async (ctx, args) => {\n    return await ctx.db.insert('files', {\n      storageId: args.storageId,\n      fileName: args.fileName,\n      fileType: args.fileType,\n      fileSize: args.fileSize,\n      uploadedAt: Date.now(),\n    })\n  },\n})\n```\n\n### Serving Files via URL\n\n```typescript\n\u002F\u002F convex\u002Ffiles.ts\nexport const getFileUrl = query({\n  args: { storageId: v.id('_storage') },\n  returns: v.union(v.string(), v.null()),\n  handler: async (ctx, args) => {\n    return await ctx.storage.getUrl(args.storageId)\n  },\n})\n\n\u002F\u002F Get file with URL\nexport const getFile = query({\n  args: { fileId: v.id('files') },\n  returns: v.union(\n    v.object({\n      _id: v.id('files'),\n      fileName: v.string(),\n      fileType: v.string(),\n      fileSize: v.number(),\n      url: v.union(v.string(), v.null()),\n    }),\n    v.null(),\n  ),\n  handler: async (ctx, args) => {\n    const file = await ctx.db.get(args.fileId)\n    if (!file) return null\n\n    const url = await ctx.storage.getUrl(file.storageId)\n\n    return {\n      _id: file._id,\n      fileName: file.fileName,\n      fileType: file.fileType,\n      fileSize: file.fileSize,\n      url,\n    }\n  },\n})\n```\n\n### Displaying Files in React\n\n```typescript\nimport { useQuery } from \"convex\u002Freact\";\nimport { api } from \"..\u002Fconvex\u002F_generated\u002Fapi\";\n\nfunction FileDisplay({ fileId }: { fileId: Id\u003C\"files\"> }) {\n  const file = useQuery(api.files.getFile, { fileId });\n\n  if (!file) return \u003Cdiv>Loading...\u003C\u002Fdiv>;\n  if (!file.url) return \u003Cdiv>File not found\u003C\u002Fdiv>;\n\n  \u002F\u002F Handle different file types\n  if (file.fileType.startsWith(\"image\u002F\")) {\n    return \u003Cimg src={file.url} alt={file.fileName} \u002F>;\n  }\n\n  if (file.fileType === \"application\u002Fpdf\") {\n    return (\n      \u003Ciframe\n        src={file.url}\n        title={file.fileName}\n        width=\"100%\"\n        height=\"600px\"\n      \u002F>\n    );\n  }\n\n  return (\n    \u003Ca href={file.url} download={file.fileName}>\n      Download {file.fileName}\n    \u003C\u002Fa>\n  );\n}\n```\n\n### Storing Generated Files from Actions\n\n```typescript\n\u002F\u002F convex\u002Fgenerate.ts\n'use node'\n\nimport { action } from '.\u002F_generated\u002Fserver'\nimport { v } from 'convex\u002Fvalues'\nimport { api } from '.\u002F_generated\u002Fapi'\n\nexport const generatePDF = action({\n  args: { content: v.string() },\n  returns: v.id('_storage'),\n  handler: async (ctx, args) => {\n    \u002F\u002F Generate PDF (example using a library)\n    const pdfBuffer = await generatePDFFromContent(args.content)\n\n    \u002F\u002F Convert to Blob\n    const blob = new Blob([pdfBuffer], { type: 'application\u002Fpdf' })\n\n    \u002F\u002F Store in Convex\n    const storageId = await ctx.storage.store(blob)\n\n    return storageId\n  },\n})\n\n\u002F\u002F Generate and save image\nexport const generateImage = action({\n  args: { prompt: v.string() },\n  returns: v.id('_storage'),\n  handler: async (ctx, args) => {\n    \u002F\u002F Call external API to generate image\n    const response = await fetch('https:\u002F\u002Fapi.example.com\u002Fgenerate', {\n      method: 'POST',\n      body: JSON.stringify({ prompt: args.prompt }),\n    })\n\n    const imageBuffer = await response.arrayBuffer()\n    const blob = new Blob([imageBuffer], { type: 'image\u002Fpng' })\n\n    return await ctx.storage.store(blob)\n  },\n})\n```\n\n### Accessing File Metadata\n\n```typescript\n\u002F\u002F convex\u002Ffiles.ts\nimport { query } from '.\u002F_generated\u002Fserver'\nimport { v } from 'convex\u002Fvalues'\nimport { Id } from '.\u002F_generated\u002FdataModel'\n\ntype FileMetadata = {\n  _id: Id\u003C'_storage'>\n  _creationTime: number\n  contentType?: string\n  sha256: string\n  size: number\n}\n\nexport const getFileMetadata = query({\n  args: { storageId: v.id('_storage') },\n  returns: v.union(\n    v.object({\n      _id: v.id('_storage'),\n      _creationTime: v.number(),\n      contentType: v.optional(v.string()),\n      sha256: v.string(),\n      size: v.number(),\n    }),\n    v.null(),\n  ),\n  handler: async (ctx, args) => {\n    const metadata = await ctx.db.system.get(args.storageId)\n    return metadata as FileMetadata | null\n  },\n})\n```\n\n### Deleting Files\n\n```typescript\n\u002F\u002F convex\u002Ffiles.ts\nimport { mutation } from '.\u002F_generated\u002Fserver'\nimport { v } from 'convex\u002Fvalues'\n\nexport const deleteFile = mutation({\n  args: { fileId: v.id('files') },\n  returns: v.null(),\n  handler: async (ctx, args) => {\n    const file = await ctx.db.get(args.fileId)\n    if (!file) return null\n\n    \u002F\u002F Delete from storage\n    await ctx.storage.delete(file.storageId)\n\n    \u002F\u002F Delete database record\n    await ctx.db.delete(args.fileId)\n\n    return null\n  },\n})\n```\n\n### Image Upload with Preview\n\n```typescript\nimport { useMutation } from \"convex\u002Freact\";\nimport { api } from \"..\u002Fconvex\u002F_generated\u002Fapi\";\nimport { useState, useRef } from \"react\";\n\nfunction ImageUploader({ onUpload }: { onUpload: (id: Id\u003C\"files\">) => void }) {\n  const generateUploadUrl = useMutation(api.files.generateUploadUrl);\n  const saveFile = useMutation(api.files.saveFile);\n  const [preview, setPreview] = useState\u003Cstring | null>(null);\n  const [uploading, setUploading] = useState(false);\n  const inputRef = useRef\u003CHTMLInputElement>(null);\n\n  const handleFileSelect = async (e: React.ChangeEvent\u003CHTMLInputElement>) => {\n    const file = e.target.files?.[0];\n    if (!file) return;\n\n    \u002F\u002F Validate file type\n    if (!file.type.startsWith(\"image\u002F\")) {\n      alert(\"Please select an image file\");\n      return;\n    }\n\n    \u002F\u002F Validate file size (max 10MB)\n    if (file.size > 10 * 1024 * 1024) {\n      alert(\"File size must be less than 10MB\");\n      return;\n    }\n\n    \u002F\u002F Show preview\n    const reader = new FileReader();\n    reader.onload = (e) => setPreview(e.target?.result as string);\n    reader.readAsDataURL(file);\n\n    \u002F\u002F Upload\n    setUploading(true);\n    try {\n      const uploadUrl = await generateUploadUrl();\n      const result = await fetch(uploadUrl, {\n        method: \"POST\",\n        headers: { \"Content-Type\": file.type },\n        body: file,\n      });\n\n      const { storageId } = await result.json();\n      const fileId = await saveFile({\n        storageId,\n        fileName: file.name,\n        fileType: file.type,\n        fileSize: file.size,\n      });\n\n      onUpload(fileId);\n    } finally {\n      setUploading(false);\n    }\n  };\n\n  return (\n    \u003Cdiv>\n      \u003Cinput\n        ref={inputRef}\n        type=\"file\"\n        accept=\"image\u002F*\"\n        onChange={handleFileSelect}\n        style={{ display: \"none\" }}\n      \u002F>\n\n      \u003Cbutton\n        onClick={() => inputRef.current?.click()}\n        disabled={uploading}\n      >\n        {uploading ? \"Uploading...\" : \"Select Image\"}\n      \u003C\u002Fbutton>\n\n      {preview && (\n        \u003Cimg\n          src={preview}\n          alt=\"Preview\"\n          style={{ maxWidth: 200, marginTop: 10 }}\n        \u002F>\n      )}\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n## Examples\n\n### Schema for File Storage\n\n```typescript\n\u002F\u002F convex\u002Fschema.ts\nimport { defineSchema, defineTable } from 'convex\u002Fserver'\nimport { v } from 'convex\u002Fvalues'\n\nexport default defineSchema({\n  files: defineTable({\n    storageId: v.id('_storage'),\n    fileName: v.string(),\n    fileType: v.string(),\n    fileSize: v.number(),\n    uploadedBy: v.id('users'),\n    uploadedAt: v.number(),\n  })\n    .index('by_user', ['uploadedBy'])\n    .index('by_type', ['fileType']),\n\n  \u002F\u002F User avatars\n  users: defineTable({\n    name: v.string(),\n    email: v.string(),\n    avatarStorageId: v.optional(v.id('_storage')),\n  }),\n\n  \u002F\u002F Posts with images\n  posts: defineTable({\n    authorId: v.id('users'),\n    content: v.string(),\n    imageStorageIds: v.array(v.id('_storage')),\n    createdAt: v.number(),\n  }).index('by_author', ['authorId']),\n})\n```\n\n## Best Practices\n\n- Never run `npx convex deploy` unless explicitly instructed\n- Never run any git commands unless explicitly instructed\n- Validate file types and sizes on the client before uploading\n- Store file metadata (name, type, size) in your own table\n- Use the `_storage` system table only for Convex metadata\n- Delete storage files when deleting database references\n- Use appropriate Content-Type headers when uploading\n- Consider image optimization for large images\n\n## Common Pitfalls\n\n1. **Not setting Content-Type header** - Files may not serve correctly\n2. **Forgetting to delete storage** - Orphaned files waste storage\n3. **Not validating file types** - Security risk for malicious uploads\n4. **Large file uploads without progress** - Poor UX for users\n5. **Using deprecated getMetadata** - Use ctx.db.system.get instead\n\n## References\n\n- Convex Documentation: https:\u002F\u002Fdocs.convex.dev\u002F\n- Convex LLMs.txt: https:\u002F\u002Fdocs.convex.dev\u002Fllms.txt\n- File Storage: https:\u002F\u002Fdocs.convex.dev\u002Ffile-storage\n- Upload Files: https:\u002F\u002Fdocs.convex.dev\u002Ffile-storage\u002Fupload-files\n- Serve Files: https:\u002F\u002Fdocs.convex.dev\u002Ffile-storage\u002Fserve-files\n",{"name":5,"tags":36,"author":37,"version":38,"description":7,"displayName":39},[19,29,30,27,28],"Convex","1.0.0","Convex File Storage","main","83212e6d909b6a0fe9e93fd5a3ff1723300f5b5b",".claude\u002Fskills\u002Fconvex-file-storage","https:\u002F\u002Fgithub.com\u002Fbaisharg\u002FASTN","https:\u002F\u002Fsafetytalent.org","https:\u002F\u002Fgithub.com\u002Fbaisharg","2026-11-13 01:54:27",{"source":8,"skillName":5,"commands":48,"prompt":58,"zipAvailable":59},[49,52,55],{"tool":50,"command":51},"npx","npx skills add baisharg\u002FASTN --skill convex-file-storage",{"tool":53,"command":54},"bunx","bunx skills add baisharg\u002FASTN --skill convex-file-storage",{"tool":56,"command":57},"pnpm","pnpm dlx skills add baisharg\u002FASTN --skill convex-file-storage","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fbaisharg\u002FASTN\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fconvex-file-storage\u002FSKILL.md",true,[],[62,84,98,112,126,143],{"skillNo":63,"name":64,"nameZh":6,"description":65,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":66,"categories":67,"keywords":73,"publishedAt":31},"01M6CCE5DVER8DQWJDY51HAJF7","schema-builder","Design and generate Convex database schemas with proper validation, indexes, and relationships. Use when creating schema.ts or modifying table definitions.","https:\u002F\u002Fgithub.com\u002Fbaisharg\u002FASTN\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fschema-builder\u002FSKILL.md",[68],{"categoryNo":69,"categoryName":70,"taxonomyType":71,"taxonomyTypeDesc":72},"01JBSEEDCAT000000000000015","视觉设计","SCENARIO","使用场景",[74,75,76,77,78,79,80,81,82,19,83],"schema builder","schema","builder","indexes","validation","use","table","database","design","generate",{"skillNo":85,"name":86,"nameZh":6,"description":87,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":88,"categories":89,"keywords":90,"publishedAt":31},"01M6CCE36AA57CG36R4VHKFRXV","integration-tanstack-start","PostHog integration for TanStack Start full-stack applications","https:\u002F\u002Fgithub.com\u002Fbaisharg\u002FASTN\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fposthog-integration-tanstack-start\u002FSKILL.md",[],[91,92,93,94,95,96,97],"integration","start","tanstack","integration tanstack start","posthog","applications","stack",{"skillNo":99,"name":100,"nameZh":6,"description":101,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":102,"categories":103,"keywords":104,"publishedAt":31},"01M6CCE0FK6N8XAB093X7TTQ4F","migration-helper","Plan and execute Convex schema migrations safely, including adding fields, creating tables, and data transformations. Use when schema changes affect existing data.","https:\u002F\u002Fgithub.com\u002Fbaisharg\u002FASTN\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fmigration-helper\u002FSKILL.md",[],[105,106,107,108,109,110,79,75,111,19],"migration","migration helper","schema migrations safely","data","plan","execute","migrations",{"skillNo":113,"name":114,"nameZh":6,"description":115,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":116,"categories":117,"keywords":118,"publishedAt":31},"01M6CCDY3644G8WXXP0BDNMZPR","gsd:sprint","Autonomous phase execution with Codex validation. Runs plan\u002Fexecute\u002Fverify cycles in a bash loop until done or halted.","https:\u002F\u002Fgithub.com\u002Fbaisharg\u002FASTN\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fgsd-sprint\u002FSKILL.md",[],[114,119,120,78,92,121,122,123,124,109,125,110],"resume","codex","execution","verify","Bash","loop","autonomous",{"skillNo":127,"name":128,"nameZh":6,"description":129,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":130,"categories":131,"keywords":132,"publishedAt":31},"01M6CCDVTZRTFWWGREMV6FF956","gsd:second-opinion","Get a second opinion from OpenAI Codex (GPT-5.2-codex) on code, architecture, or implementation decisions. Use when you want independent verification or an alternative perspective.","https:\u002F\u002Fgithub.com\u002Fbaisharg\u002FASTN\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fgsd-second-opinion\u002FSKILL.md",[],[133,134,135,120,136,137,138,139,140,141,79,142],"gsd:second opinion","second opinion","question","architecture","code","verification","openai","gpt","decisions","implementation",{"skillNo":144,"name":145,"nameZh":6,"description":146,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":147,"categories":148,"keywords":149,"publishedAt":31},"01M6CCDSDPYYNEDERHP01N30PT","gsd:milestone-sprint","Run entire milestone autonomously with Codex validation. Auto-detects current milestone, executes all phases, and runs audit.","https:\u002F\u002Fgithub.com\u002Fbaisharg\u002FASTN\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fgsd-milestone-sprint\u002FSKILL.md",[],[150,151,78,152,153,154,119,120],"gsd:milestone sprint","run","audit","interactive","auto"]