{"id":"convex-file-storage","name":"convex-file-storage","summary":"アップロードフロー、URL経由のファイル提供、アクション生成ファイルの保存、削除、システムテーブルからのファイルメタデータアクセスを含む完全なファイル処理","body":"# 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://docs.convex.dev/file-storage\n- Upload Files: https://docs.convex.dev/file-storage/upload-files\n- Serve Files: https://docs.convex.dev/file-storage/serve-files\n- For broader context: https://docs.convex.dev/llms.txt\n\n## Instructions\n\n### File Storage Overview\n\nConvex provides built-in file storage with:\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// convex/files.ts\nimport { mutation } from \"./_generated/server\";\nimport { v } from \"convex/values\";\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// React component\nimport { useMutation } from \"convex/react\";\nimport { api } from \"../convex/_generated/api\";\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<HTMLInputElement>) => {\n    const file = e.target.files?.[0];\n    if (!file) return;\n\n    setUploading(true);\n    try {\n      // Step 1: Get upload URL\n      const uploadUrl = await generateUploadUrl();\n\n      // 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      // 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    <div>\n      <input\n        type=\"file\"\n        onChange={handleUpload}\n        disabled={uploading}\n      />\n      {uploading && <p>Uploading...</p>}\n    </div>\n  );\n}\n```\n\n### Saving File References\n\n```typescript\n// convex/files.ts\nimport { mutation, query } from \"./_generated/server\";\nimport { v } from \"convex/values\";\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// convex/files.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// 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/react\";\nimport { api } from \"../convex/_generated/api\";\n\nfunction FileDisplay({ fileId }: { fileId: Id<\"files\"> }) {\n  const file = useQuery(api.files.getFile, { fileId });\n\n  if (!file) return <div>Loading...</div>;\n  if (!file.url) return <div>File not found</div>;\n\n  // Handle different file types\n  if (file.fileType.startsWith(\"image/\")) {\n    return <img src={file.url} alt={file.fileName} />;\n  }\n\n  if (file.fileType === \"application/pdf\") {\n    return (\n      <iframe\n        src={file.url}\n        title={file.fileName}\n        width=\"100%\"\n        height=\"600px\"\n      />\n    );\n  }\n\n  return (\n    <a href={file.url} download={file.fileName}>\n      Download {file.fileName}\n    </a>\n  );\n}\n```\n\n### Storing Generated Files from Actions\n\n```typescript\n// convex/generate.ts\n\"use node\";\n\nimport { action } from \"./_generated/server\";\nimport { v } from \"convex/values\";\nimport { api } from \"./_generated/api\";\n\nexport const generatePDF = action({\n  args: { content: v.string() },\n  returns: v.id(\"_storage\"),\n  handler: async (ctx, args) => {\n    // Generate PDF (example using a library)\n    const pdfBuffer = await generatePDFFromContent(args.content);\n\n    // Convert to Blob\n    const blob = new Blob([pdfBuffer], { type: \"application/pdf\" });\n\n    // Store in Convex\n    const storageId = await ctx.storage.store(blob);\n\n    return storageId;\n  },\n});\n\n// Generate and save image\nexport const generateImage = action({\n  args: { prompt: v.string() },\n  returns: v.id(\"_storage\"),\n  handler: async (ctx, args) => {\n    // Call external API to generate image\n    const response = await fetch(\"https://api.example.com/generate\", {\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/png\" });\n\n    return await ctx.storage.store(blob);\n  },\n});\n```\n\n### Accessing File Metadata\n\n```typescript\n// convex/files.ts\nimport { query } from \"./_generated/server\";\nimport { v } from \"convex/values\";\nimport { Id } from \"./_generated/dataModel\";\n\ntype FileMetadata = {\n  _id: Id<\"_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// convex/files.ts\nimport { mutation } from \"./_generated/server\";\nimport { v } from \"convex/values\";\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    // Delete from storage\n    await ctx.storage.delete(file.storageId);\n\n    // 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/react\";\nimport { api } from \"../convex/_generated/api\";\nimport { useState, useRef } from \"react\";\n\nfunction ImageUploader({ onUpload }: { onUpload: (id: Id<\"files\">) => void }) {\n  const generateUploadUrl = useMutation(api.files.generateUploadUrl);\n  const saveFile = useMutation(api.files.saveFile);\n  const [preview, setPreview] = useState<string | null>(null);\n  const [uploading, setUploading] = useState(false);\n  const inputRef = useRef<HTMLInputElement>(null);\n\n  const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {\n    const file = e.target.files?.[0];\n    if (!file) return;\n\n    // Validate file type\n    if (!file.type.startsWith(\"image/\")) {\n      alert(\"Please select an image file\");\n      return;\n    }\n\n    // 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    // Show preview\n    const reader = new FileReader();\n    reader.onload = (e) => setPreview(e.target?.result as string);\n    reader.readAsDataURL(file);\n\n    // 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    <div>\n      <input\n        ref={inputRef}\n        type=\"file\"\n        accept=\"image/*\"\n        onChange={handleFileSelect}\n        style={{ display: \"none\" }}\n      />\n      \n      <button\n        onClick={() => inputRef.current?.click()}\n        disabled={uploading}\n      >\n        {uploading ? \"Uploading...\" : \"Select Image\"}\n      </button>\n\n      {preview && (\n        <img\n          src={preview}\n          alt=\"Preview\"\n          style={{ maxWidth: 200, marginTop: 10 }}\n        />\n      )}\n    </div>\n  );\n}\n```\n\n## Examples\n\n### Schema for File Storage\n\n```typescript\n// convex/schema.ts\nimport { defineSchema, defineTable } from \"convex/server\";\nimport { v } from \"convex/values\";\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  // User avatars\n  users: defineTable({\n    name: v.string(),\n    email: v.string(),\n    avatarStorageId: v.optional(v.id(\"_storage\")),\n  }),\n\n  // 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://docs.convex.dev/\n- Convex LLMs.txt: https://docs.convex.dev/llms.txt\n- File Storage: https://docs.convex.dev/file-storage\n- Upload Files: https://docs.convex.dev/file-storage/upload-files\n- Serve Files: https://docs.convex.dev/file-storage/serve-files","author":"@waynesutton","ownerProfile":null,"authorContacts":null,"sourceUrl":"https://github.com/waynesutton/convexskills/tree/main/skills/convex-file-storage","license":"Apache-2.0","category":"document","lang":"en","tokens":2818,"stars":0,"calls30d":1,"claimed":false,"visibility":"public","origin":"crawler","version":"0.1.0","createdAt":"2026-08-22","updatedAt":"2026-08-22","files":[{"path":"agents/openai.yaml","size":91,"sha256":"bb57e6929f0916464111ae4a5e0a2ec5d301653b5a3b818a81dc2c0a7deb21c2"}],"requires":{"mcp":[],"tools":[]},"safety":{"flags":[],"scannedAt":"2026-08-22","hasScripts":false,"networkEndpoints":["api.example.com","docs.convex.dev"]}}