> ## 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.

# Storage API

> API reference for file storage in Convex

The storage API allows you to store and retrieve files in Convex.

## Reading files

File reading is available in queries, mutations, and actions via `ctx.storage`.

### getUrl

Get a URL for downloading a file.

```typescript theme={null}
const url = await ctx.storage.getUrl(storageId);
if (url === null) {
  throw new Error("File not found");
}
// Use the URL (e.g., return it to the client)
```

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

<ResponseField name="return" type="string | null">
  A URL which fetches the file via HTTP GET, or `null` if the file no longer exists.

  The GET response includes a standard HTTP Digest header with a SHA-256 checksum.
</ResponseField>

### getMetadata (deprecated)

Get metadata for a file.

```typescript theme={null}
const metadata = await ctx.storage.getMetadata(storageId);
```

**Deprecated:** Use `ctx.db.system.get("_storage", storageId)` instead:

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

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

<ResponseField name="return" type="FileMetadata | null">
  File metadata object or `null` if not found.

  <ResponseField name="storageId" type="string">
    ID for referencing the file.
  </ResponseField>

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

  <ResponseField name="size" type="number">
    Size of the file in bytes.
  </ResponseField>

  <ResponseField name="contentType" type="string | null">
    Content type of the file if provided on upload.
  </ResponseField>
</ResponseField>

## Writing files

File writing is available in mutations via `ctx.storage`.

### generateUploadUrl

Generate a short-lived URL for uploading a file.

```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();
  },
});
```

On the client, POST the file to this URL:

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

<ResponseField name="return" type="string">
  A short-lived URL for uploading a file via HTTP POST.

  The client should POST the file as the request body. The response will be JSON containing the newly allocated `storageId`:

  ```json theme={null}
  { "storageId": "kg2..." }
  ```
</ResponseField>

### delete

Delete a file from storage.

```typescript theme={null}
await ctx.storage.delete(storageId);
```

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

Once a file is deleted, any URLs previously generated by `getUrl()` will return 404 errors.

## Action-specific methods

In actions, `ctx.storage` has additional methods not available in mutations.

### get

Download a file as a Blob.

```typescript theme={null}
import { action } from "./_generated/server";
import { v } from "convex/values";

export const downloadAndProcess = action({
  args: { storageId: v.id("_storage") },
  handler: async (ctx, args) => {
    const blob = await ctx.storage.get(args.storageId);
    if (blob === null) {
      throw new Error("File not found");
    }
    
    // Process the file
    const text = await blob.text();
    console.log(text);
  },
});
```

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

<ResponseField name="return" type="Blob | null">
  A Blob containing the file contents, or `null` if the file doesn't exist.
</ResponseField>

**Note:** Only available in actions and HTTP actions, not in mutations or queries.

### store

Upload a Blob directly to storage.

```typescript theme={null}
import { action } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values";

export const uploadFromUrl = action({
  args: { url: v.string() },
  returns: v.id("_storage"),
  handler: async (ctx, args) => {
    // Fetch from external URL
    const response = await fetch(args.url);
    const blob = await response.blob();
    
    // Upload to Convex storage
    const storageId = await ctx.storage.store(blob);
    
    // Save to database via mutation
    await ctx.runMutation(internal.files.saveMetadata, {
      storageId,
      url: args.url,
    });
    
    return storageId;
  },
});
```

<ParamField path="blob" type="Blob" required>
  The Blob to store.
</ParamField>

<ParamField path="options" type="object" optional>
  <ParamField path="sha256" type="string" optional>
    Expected SHA-256 hash to verify file integrity.
  </ParamField>
</ParamField>

<ResponseField name="return" type="Id<'_storage'>">
  The ID of the newly stored file.
</ResponseField>

**Note:** Only available in actions and HTTP actions. For client-side uploads from mutations, use `generateUploadUrl()` instead.

## File metadata

File metadata is stored in the `_storage` system table. Query it like any other table:

```typescript theme={null}
const metadata = await ctx.db.system.get(storageId);
if (metadata) {
  console.log(metadata._id);           // Id<"_storage">
  console.log(metadata._creationTime); // number
  console.log(metadata.sha256);        // string
  console.log(metadata.size);          // number
  console.log(metadata.contentType);   // string | undefined
}
```

You can also query all files:

```typescript theme={null}
const allFiles = await ctx.db.system.query("_storage").collect();
```

## Common patterns

### Upload workflow

Typical file upload flow:

1. Client requests upload URL:

```typescript theme={null}
// Client
const uploadUrl = await convex.mutation(api.files.generateUploadUrl);
```

2. Client uploads file:

```typescript theme={null}
// Client
const result = await fetch(uploadUrl, {
  method: "POST",
  headers: { "Content-Type": file.type },
  body: file,
});
const { storageId } = await result.json();
```

3. Client saves to database:

```typescript theme={null}
// Client
await convex.mutation(api.files.saveFile, {
  storageId,
  name: file.name,
  type: file.type,
});
```

### Image gallery

```typescript theme={null}
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";

export const saveImage = mutation({
  args: {
    storageId: v.id("_storage"),
    caption: v.string(),
  },
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error("Not authenticated");
    
    await ctx.db.insert("images", {
      storageId: args.storageId,
      caption: args.caption,
      userId: identity.tokenIdentifier,
    });
  },
});

export const listImages = query({
  args: {},
  handler: async (ctx) => {
    const images = await ctx.db.query("images").order("desc").take(50);
    
    // Add URLs to each image
    return await Promise.all(
      images.map(async (image) => ({
        ...image,
        url: await ctx.storage.getUrl(image.storageId),
      }))
    );
  },
});
```

### File processing in actions

```typescript theme={null}
import { action } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values";
import sharp from "sharp";

export const generateThumbnail = action({
  args: { imageId: v.id("_storage") },
  returns: v.id("_storage"),
  handler: async (ctx, args) => {
    // Download original image
    const blob = await ctx.storage.get(args.imageId);
    if (!blob) throw new Error("Image not found");
    
    // Process with sharp
    const buffer = Buffer.from(await blob.arrayBuffer());
    const thumbnail = await sharp(buffer)
      .resize(200, 200, { fit: "cover" })
      .jpeg({ quality: 80 })
      .toBuffer();
    
    // Upload thumbnail
    const thumbnailBlob = new Blob([thumbnail], { type: "image/jpeg" });
    const thumbnailId = await ctx.storage.store(thumbnailBlob);
    
    // Save reference in database
    await ctx.runMutation(internal.images.saveThumbnail, {
      originalId: args.imageId,
      thumbnailId,
    });
    
    return thumbnailId;
  },
});
```

### Delete file with cleanup

```typescript theme={null}
import { mutation } from "./_generated/server";
import { v } from "convex/values";

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 from database
    await ctx.db.delete(args.imageId);
  },
});
```

## Best practices

<Card title="Store metadata separately" icon="database">
  Create a table to store file metadata (name, description, owner, etc.) and reference the `storageId`. Don't rely solely on the `_storage` system table.
</Card>

<Card title="Clean up deleted files" icon="trash">
  When deleting database records that reference files, also delete the files from storage to avoid orphaned files.
</Card>

<Card title="Validate file types" icon="shield-check">
  Check file types and sizes on the client before uploading, and validate again in your mutation after upload.
</Card>

<Card title="Use actions for processing" icon="gear">
  Process files (resize images, extract text, etc.) in actions where you can use Node.js libraries.
</Card>

<Card title="Set appropriate content types" icon="file">
  Set the `Content-Type` header when uploading to ensure files are served with the correct MIME type.
</Card>
