convex-file-storage

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.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "convex-file-storage" with this command: npx skills add aaronvanston/skills-convex/aaronvanston-skills-convex-convex-file-storage

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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

convex-realtime

No summary provided by upstream source.

Repository SourceNeeds Review
General

convex-cron

No summary provided by upstream source.

Repository SourceNeeds Review
General

convex-migrations

No summary provided by upstream source.

Repository SourceNeeds Review
General

convex-review

No summary provided by upstream source.

Repository SourceNeeds Review