> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/get-convex/convex-backend/llms.txt
> Use this file to discover all available pages before exploring further.

# File storage capabilities

> Upload, download, and manage files with Convex storage

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:

```typescript theme={null}
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
  },
});
```

<ParamField path="storageId" type="Id<'_storage'>">
  The ID of the file to fetch from Convex storage.
</ParamField>

**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):

```typescript theme={null}
const metadata = await ctx.db.system.get(storageId);
// metadata: { _id, _creationTime, sha256, size, contentType? }
```

The metadata document contains:

<ParamField path="_id" type="Id<'_storage'>">
  The storage ID of the file.
</ParamField>

<ParamField path="_creationTime" type="number">
  Timestamp when the file was uploaded (milliseconds since epoch).
</ParamField>

<ParamField path="sha256" type="string">
  Hex-encoded SHA-256 checksum of file contents.
</ParamField>

<ParamField path="size" type="number">
  Size of the file in bytes.
</ParamField>

<ParamField path="contentType" type="string | null">
  Content type of the file if it was provided on upload (e.g., `"image/png"`), or `null` if not specified.
</ParamField>

**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:

```typescript theme={null}
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:

```typescript theme={null}
// 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">`:

```json theme={null}
{ "storageId": "kg2h4..." }
```

### Delete file

Delete a file from Convex storage:

```typescript theme={null}
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);
  },
});
```

<ParamField path="storageId" type="Id<'_storage'>">
  The ID of the file to delete from Convex storage.
</ParamField>

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:

```typescript theme={null}
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 };
  },
});
```

<ParamField path="storageId" type="Id<'_storage'>">
  The ID of the file to download.
</ParamField>

**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:

```typescript theme={null}
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;
  },
});
```

<ParamField path="blob" type="Blob">
  The Blob to store in Convex storage.
</ParamField>

<ParamField path="options" type="{ sha256?: string }" optional>
  Optional settings. Pass `sha256` (hex-encoded) to verify file integrity during upload.
</ParamField>

**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)

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

### 2. Upload file (client)

```typescript theme={null}
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)

```typescript theme={null}
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

```typescript theme={null}
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

```typescript theme={null}
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

```typescript theme={null}
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.
