> ## Documentation Index
> Fetch the complete documentation index at: https://vlyai-1c28d863.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# File storage

> Upload, store, and serve files. Built-in image transformations. No S3 setup required.

Convex includes managed file storage. Upload via mutation, get back a storage ID, serve via Convex's CDN. No S3, R2, or Cloudflare setup required.

## How it works

<Steps>
  <Step title="Generate an upload URL">
    A Convex mutation calls `ctx.storage.generateUploadUrl()` and returns a one-time URL.
  </Step>

  <Step title="Client uploads directly">
    `await fetch(uploadUrl, { method: "POST", body: file })` — bytes go straight to Convex storage, not through your function.
  </Step>

  <Step title="Save the storage ID">
    Mutation stores `storageId` on a row in your schema (e.g., `users.avatarStorageId`).
  </Step>

  <Step title="Serve via URL">
    Read with `ctx.storage.getUrl(storageId)` — returns a CDN URL good for serving.
  </Step>
</Steps>

## Example

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

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

export const setAvatar = mutation({
  args: { storageId: v.id("_storage") },
  handler: async (ctx, { storageId }) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error("Not signed in");
    await ctx.db.patch(identity.subject as Id<"users">, { avatarStorageId: storageId });
  },
});

export const getAvatarUrl = query({
  args: { userId: v.id("users") },
  handler: async (ctx, { userId }) => {
    const user = await ctx.db.get(userId);
    if (!user?.avatarStorageId) return null;
    return await ctx.storage.getUrl(user.avatarStorageId);
  },
});
```

Client:

```typescript theme={null}
const generateUploadUrl = useMutation(api.files.generateUploadUrl);
const setAvatar         = useMutation(api.files.setAvatar);

async function uploadAvatar(file: File) {
  const url     = await generateUploadUrl();
  const result  = await fetch(url, { method: "POST", body: file });
  const { storageId } = await result.json();
  await setAvatar({ storageId });
}
```

## Limits

| Item | Default |
| - | - |
| Max file size | 100 MB |
| Total storage per project | Plan-dependent |
| Bandwidth | Plan-dependent |

For files larger than 100 MB, use a multi-part upload pattern or store in S3 / R2 via [the AWS S3 integration](/integrations/aws-s3).

## Image transformations

Append query params to the URL for on-the-fly transformations:

```
?w=400         # resize to 400px wide
?fit=cover     # crop to fit
?format=webp   # convert format
```

For more advanced needs (multiple breakpoints, HEIC conversion), use [Cloudinary](/integrations/cloudinary).

## Tips

<Tip>
  **Always validate file type and size on the server.** Client-side checks are user-friendly but bypassable.
</Tip>

<Tip>
  **Store file metadata separately.** Filename, MIME type, original dimensions — store these alongside the storage ID for easier display.
</Tip>

## Related

<CardGroup cols={3}>
  <Card title="File uploads recipe" icon="upload" href="/recipes/file-uploads">
    Complete walkthrough.
  </Card>

  <Card title="Cloudinary" icon="image" href="/integrations/cloudinary">
    For advanced image transformations.
  </Card>

  <Card title="Convex actions" icon="zap" href="/convex/actions">
    For uploads that need to call third-party APIs.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.