convex-file-storage
Convexのファイルストレージを活用し、ファイルアップロード機能の実装、アップロードURLの生成、ファイル配信、メタデータ管理など、アバターや添付ファイルといったファイル関連機能を構築するSkill。
📜 元の英語説明(参考)
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.
🇯🇵 日本人クリエイター向け解説
Convexのファイルストレージを活用し、ファイルアップロード機能の実装、アップロードURLの生成、ファイル配信、メタデータ管理など、アバターや添付ファイルといったファイル関連機能を構築するSkill。
※ jpskill.com 編集部が日本のビジネス現場向けに補足した解説です。Skill本体の挙動とは独立した参考情報です。
下記のコマンドをコピーしてターミナル(Mac/Linux)または PowerShell(Windows)に貼り付けてください。 ダウンロード → 解凍 → 配置まで全自動。
mkdir -p ~/.claude/skills && cd ~/.claude/skills && curl -L -o convex-file-storage.zip https://jpskill.com/download/8730.zip && unzip -o convex-file-storage.zip && rm convex-file-storage.zip
$d = "$env:USERPROFILE\.claude\skills"; ni -Force -ItemType Directory $d | Out-Null; iwr https://jpskill.com/download/8730.zip -OutFile "$d\convex-file-storage.zip"; Expand-Archive "$d\convex-file-storage.zip" -DestinationPath $d -Force; ri "$d\convex-file-storage.zip"
完了後、Claude Code を再起動 → 普通に「動画プロンプト作って」のように話しかけるだけで自動発動します。
💾 手動でダウンロードしたい(コマンドが難しい人向け)
- 1. 下の青いボタンを押して
convex-file-storage.zipをダウンロード - 2. ZIPファイルをダブルクリックで解凍 →
convex-file-storageフォルダができる - 3. そのフォルダを
C:\Users\あなたの名前\.claude\skills\(Win)または~/.claude/skills/(Mac)へ移動 - 4. Claude Code を再起動
⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。
🎯 このSkillでできること
下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。
📦 インストール方法 (3ステップ)
- 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
- 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
- 3. 展開してできたフォルダを、ホームフォルダの
.claude/skills/に置く- · macOS / Linux:
~/.claude/skills/ - · Windows:
%USERPROFILE%\.claude\skills\
- · macOS / Linux:
Claude Code を再起動すれば完了。「このSkillを使って…」と話しかけなくても、関連する依頼で自動的に呼び出されます。
詳しい使い方ガイドを見る →- 最終更新
- 2026-05-18
- 取得日時
- 2026-05-18
- 同梱ファイル
- 1
📖 Skill本文(日本語訳)
※ 原文(英語/中国語)を Gemini で日本語化したものです。Claude 自身は原文を読みます。誤訳がある場合は原文をご確認ください。
Convex ファイルストレージ
アップロードフロー
1. アップロード URL の生成 (Mutation)
// 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();
},
});
2. クライアント側のアップロード
// 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;
}
3. ファイル参照の保存 (Mutation)
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(),
});
},
});
ファイルの提供
ファイル URL の取得 (Query)
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>
);
}
HTTP ファイル提供
// 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;
ファイルサイズの制限
- デフォルトの最大ファイルサイズ: 20MB
- より大きなファイルの場合は、チャンクアップロードまたは外部ストレージを使用してください。
よくある落とし穴
- ストレージの削除忘れ - メタデータとストレージ BLOB の両方を必ず削除してください。
- ファイル形式の検証不足 - クライアントとサーバーで検証してください。
- すべてのファイルの公開 - 所有権のチェックを追加してください。
(原文がここで切り詰められています)
📜 原文 SKILL.md(Claudeが読む英語/中国語)を展開
Convex File Storage
Upload Flow
1. Generate Upload URL (Mutation)
// 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();
},
});
2. Client Upload
// 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;
}
3. Store File Reference (Mutation)
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(),
});
},
});
Serving Files
Get File URL (Query)
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);
},
});
Serve with Metadata
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,
};
},
});
Delete Files
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;
},
});
Schema Definition
// 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"]),
});
Image Handling
With Dimensions
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(),
});
},
});
Client-Side with Preview
// 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>
);
}
HTTP File Serving
// 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;
File Size Limits
- Default max file size: 20MB
- For larger files, use chunked uploads or external storage
Common Pitfalls
- Forgetting to delete storage - Always delete both metadata and storage blob
- Not validating file types - Validate on client and server
- Exposing all files - Add ownership checks before serving
- Missing error handling - Handle upload failures gracefully
References
- File Storage: https://docs.convex.dev/file-storage
- HTTP Actions: https://docs.convex.dev/functions/http-actions