Skip to main content
Convex provides built-in file storage for images, videos, documents, and any other file type. Files are stored securely and can be accessed via temporary URLs.

Storage reader

The StorageReader interface provides read-only access to file storage. Available as ctx.storage in queries, mutations, and actions.

Get file URL

Get a URL to download a file:
import { query } from "./_generated/server";
import { v } from "convex/values";

export const getFileUrl = query({
  args: { storageId: v.id("_storage") },
  handler: async (ctx, args) => {
    const url = await ctx.storage.getUrl(args.storageId);
    if (url) {
      // Use the URL (e.g., return it to the client)
      return url;
    }
    return null; // File no longer exists
  },
});
storageId
Id<'_storage'>
The ID of the file to fetch from Convex storage.
Returns: A URL which fetches the file via HTTP GET, or null if the file no longer exists. Note: The GET response includes a standard HTTP Digest header with a SHA-256 checksum for integrity verification.

Get file metadata

Get metadata for a stored file via the system table (preferred method):
const metadata = await ctx.db.system.get(storageId);
// metadata: { _id, _creationTime, sha256, size, contentType? }
The metadata document contains:
_id
Id<'_storage'>
The storage ID of the file.
_creationTime
number
Timestamp when the file was uploaded (milliseconds since epoch).
sha256
string
Hex-encoded SHA-256 checksum of file contents.
size
number
Size of the file in bytes.
contentType
string | null
Content type of the file if it was provided on upload (e.g., "image/png"), or null if not specified.
Deprecated: ctx.storage.getMetadata() is deprecated. Use ctx.db.system.get() instead for equivalent metadata.

Storage writer

The StorageWriter interface extends StorageReader with write operations. Available as ctx.storage in mutations.

Generate upload URL

Generate a short-lived URL for uploading a file from the client:
import { mutation } from "./_generated/server";
import { v } from "convex/values";

export const generateUploadUrl = mutation({
  args: {},
  returns: v.string(),
  handler: async (ctx) => {
    return await ctx.storage.generateUploadUrl();
  },
});
Returns: A short-lived URL for uploading a file via HTTP POST. The client should POST the file as the request body to this URL:
// On the client:
const uploadUrl = await generateUploadUrl();
const result = await fetch(uploadUrl, {
  method: "POST",
  headers: { "Content-Type": file.type },
  body: file,
});
const { storageId } = await result.json();
// Save storageId to the database
The response is a JSON object containing the newly allocated Id<"_storage">:
{ "storageId": "kg2h4..." }

Delete file

Delete a file from Convex storage:
import { mutation } from "./_generated/server";
import { v } from "convex/values";

export const deleteFile = mutation({
  args: { storageId: v.id("_storage") },
  handler: async (ctx, args) => {
    await ctx.storage.delete(args.storageId);
  },
});
storageId
Id<'_storage'>
The ID of the file to delete from Convex storage.
Once a file is deleted, any URLs previously generated by getUrl() will return 404 errors.

Storage action writer

The StorageActionWriter interface extends StorageWriter with additional methods only available in actions and HTTP actions (not in queries or mutations).

Download file

Download a file from storage as a Blob in an action:
import { action } from "./_generated/server";
import { v } from "convex/values";

export const processImage = action({
  args: { storageId: v.id("_storage") },
  handler: async (ctx, args) => {
    const blob = await ctx.storage.get(args.storageId);
    if (!blob) {
      throw new Error("File not found");
    }

    // Process the blob (e.g., resize image, extract metadata)
    const arrayBuffer = await blob.arrayBuffer();
    // ...

    return { size: blob.size, type: blob.type };
  },
});
storageId
Id<'_storage'>
The ID of the file to download.
Returns: A Blob containing the file contents, or null if the file doesn’t exist. Note: This method is only available in actions and HTTP actions, not in queries or mutations.

Upload file directly

Upload a Blob directly to storage from an action:
import { action } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values";

export const downloadAndStore = action({
  args: { url: v.string() },
  handler: async (ctx, args) => {
    // Fetch file from external URL
    const response = await fetch(args.url);
    const blob = await response.blob();

    // Store in Convex storage
    const storageId = await ctx.storage.store(blob);

    // Save metadata to database via mutation
    await ctx.runMutation(internal.files.saveMetadata, {
      storageId,
      originalUrl: args.url,
    });

    return storageId;
  },
});
blob
Blob
The Blob to store in Convex storage.
options
{ sha256?: string }
Optional settings. Pass sha256 (hex-encoded) to verify file integrity during upload.
Returns: The Id<"_storage"> of the newly stored file. Note: This method is only available in actions and HTTP actions. For client-side uploads from mutations, use generateUploadUrl() instead.

File upload flow

The typical file upload flow involves three steps:

1. Generate upload URL (mutation)

export const generateUploadUrl = mutation({
  args: {},
  handler: async (ctx) => {
    return await ctx.storage.generateUploadUrl();
  },
});

2. Upload file (client)

const uploadUrl = await convex.mutation(api.files.generateUploadUrl);

const result = await fetch(uploadUrl, {
  method: "POST",
  headers: { "Content-Type": file.type },
  body: file,
});

const { storageId } = await result.json();

3. Save metadata (mutation)

export const saveFile = mutation({
  args: {
    storageId: v.id("_storage"),
    name: v.string(),
  },
  handler: async (ctx, args) => {
    // Get file metadata
    const metadata = await ctx.db.system.get(args.storageId);

    // Save to your table
    return await ctx.db.insert("files", {
      storageId: args.storageId,
      name: args.name,
      size: metadata?.size,
      contentType: metadata?.contentType,
    });
  },
});

Common patterns

Store images with metadata

import { mutation } from "./_generated/server";
import { v } from "convex/values";

export const saveImage = mutation({
  args: {
    storageId: v.id("_storage"),
    title: v.string(),
    description: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) {
      throw new Error("Not authenticated");
    }

    // Get file metadata
    const fileMetadata = await ctx.db.system.get(args.storageId);
    if (!fileMetadata) {
      throw new Error("File not found");
    }

    // Verify it's an image
    if (!fileMetadata.contentType?.startsWith("image/")) {
      throw new Error("File must be an image");
    }

    return await ctx.db.insert("images", {
      storageId: args.storageId,
      title: args.title,
      description: args.description,
      uploadedBy: identity.tokenIdentifier,
      contentType: fileMetadata.contentType,
      size: fileMetadata.size,
    });
  },
});

Delete file and metadata

export const deleteImage = mutation({
  args: { imageId: v.id("images") },
  handler: async (ctx, args) => {
    const image = await ctx.db.get(args.imageId);
    if (!image) {
      throw new Error("Image not found");
    }

    // Delete from storage
    await ctx.storage.delete(image.storageId);

    // Delete metadata
    await ctx.db.delete(args.imageId);
  },
});

Process files in actions

import { action } from "./_generated/server";
import { internal } from "./_generated/api";

export const generateThumbnail = action({
  args: { storageId: v.id("_storage") },
  handler: async (ctx, args) => {
    // Download original image
    const blob = await ctx.storage.get(args.storageId);
    if (!blob) throw new Error("File not found");

    // Process image (pseudo-code - use actual image library)
    const thumbnail = await resizeImage(blob, { width: 200, height: 200 });

    // Upload thumbnail
    const thumbnailId = await ctx.storage.store(thumbnail);

    // Update database
    await ctx.runMutation(internal.images.saveThumbnail, {
      originalId: args.storageId,
      thumbnailId,
    });

    return thumbnailId;
  },
});

Best practices

  • Use ctx.db.system.get() for metadata - Prefer querying the _storage system table over the deprecated getMetadata() method.
  • Verify file types - Check the contentType to ensure uploaded files match expected types.
  • Delete files when documents are deleted - Clean up storage when deleting database records that reference files.
  • Use actions for processing - Download and process large files in actions to avoid blocking mutations.
  • Generate short-lived URLs - Upload URLs expire, so generate them close to when they’ll be used.
  • Store minimal metadata - The _storage system table already has size and content type; only store additional app-specific metadata.

Build docs developers (and LLMs) love