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.
Scanned 9/4/2026
Install to Claude Code
npx -y skills add NeverSight/skills_feed --skill convex-file-storage --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Convex File Storage?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/neversight-convex-file-storage)More formats (shields.io, HTML) on the badges page.
---
name: convex-file-storage
description: 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 File Storage
## Upload Flow
### 1. Generate Upload URL (Mutation)
```typescript
// 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
```typescript
// 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)
```typescript
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)
```typescript
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
```typescript
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
```typescript
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
```typescript
// 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
```typescript
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
```typescript
// 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
```typescript
// 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
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!