---
title: "File Storage"
description: "Upload one file or a whole queue to Convex file storage with useUpload and useUploadQueue, and resolve a stored file’s served URL with useStorageUrl."
canonical_url: "https://nuxt-convex-module.dev/guide/file-storage"
---
# File Storage

> Upload one file or a whole queue to Convex file storage with useUpload and useUploadQueue, and resolve a stored file’s served URL with useStorageUrl.

Convenience composables over Convex's file storage, all auto-imported: `useUpload` for a single file with reactive progress, `useUploadQueue` for batches with bounded concurrency, and `useStorageUrl` to reactively resolve a served URL from a storage id. Try a couple of small images:

<callout icon="i-lucide-sparkles">

Only here — Convex's React client has no upload composables; these are additive, over the same `generateUploadUrl` + `storage` API your functions already use.

</callout>

<playground-files>



</playground-files>

```vue [components/FileUploader.vue]
<script setup lang="ts">
import { api } from '#convex/api'

const files = useQuery(api.files.list, {})
const save = useMutation(api.files.save)

// Single file: progress, error, and the resulting storage id as refs.
const { upload, isUploading, progress, error, storageId } = useUpload(
  api.files.generateUploadUrl,
  { onSuccess: (id, file) => save({ storageId: id, name: file.name, type: file.type, size: file.size }) },
)

// Resolve the served URL for the latest upload; the query is skipped
// while `storageId` is still null.
const latestUrl = useStorageUrl(api.files.url, storageId)

// Many files: enqueue a FileList, two uploads run at a time.
const { items, enqueue, progress: queueProgress } = useUploadQueue(
  api.files.generateUploadUrl,
  {
    concurrency: 2,
    onItemSuccess: (id, item) => save({ storageId: id, ...metaOf(item.file) }),
  },
)
</script>

<template>
  <input type="file" accept="image/*" :disabled="isUploading"
    @change="e => upload((e.target as HTMLInputElement).files![0])">
  <progress v-if="isUploading" :value="progress" max="1" />

  <input type="file" multiple @change="e => enqueue((e.target as HTMLInputElement).files)">
  <li v-for="item in items" :key="item.id">{{ item.status }} — {{ item.progress }}</li>

  <img v-if="latestUrl" :src="latestUrl">
</template>
```

The gallery below the pickers is a plain `useQuery` whose Convex query resolves URLs server-side, so new uploads and removals stream in live in every window you have open. It lists only your browser's uploads, and each one is deleted after an hour.

## `useUpload()`

Upload a single file. Pass your `generateUploadUrl` mutation reference — the composable calls it to obtain the one-time upload URL, `POST`s the file, and reads back the storage id:

```vue [components/SingleUpload.vue]
<script setup lang="ts">
import { api } from '#convex/api'

const { upload, progress, isUploading, error } = useUpload(api.files.generateUploadUrl)

async function onChange(e: Event) {
  const file = (e.target as HTMLInputElement).files?.[0]
  if (file) {
    const storageId = await upload(file)
    if (storageId === null) console.error(error.value)
  }
}
</script>
```

`upload()` never throws: it resolves to the new `StorageId`, or to `null` when the upload fails (or runs during SSR — uploads are client-only), setting the `error` ref with the reason.

The full return surface is `{ upload, isUploading, progress, error, storageId, cancel, reset }` — `progress` is a `0`–`1` ref, `storageId` holds the last successful id, `cancel()` aborts the in-flight upload, and `reset()` clears `progress`, `error`, and `storageId`.

An optional second argument takes callbacks — `onSuccess(storageId, file)`, `onProgress(fraction)`, and `onError(error, file)`: `useUpload(api.files.generateUploadUrl, { onSuccess: storageId => saveImage({ storageId }) })`.

## `useUploadQueue()`

Manage several uploads at once, each with its own status and progress (a deeply reactive queue):

```ts
const { items, enqueue, progress, isUploading } = useUploadQueue(
  api.files.generateUploadUrl,
  { concurrency: 4, onItemSuccess: storageId => saveImage({ storageId }) },
)
```

`enqueue` accepts a single `Blob`, a `Blob[]`, or a `FileList` straight from a multi-file `<input>` — `enqueue((event.target as HTMLInputElement).files)` — and uploading starts automatically, at most `concurrency` at a time (default `3`). Each entry in `items` carries `{ id, file, status, progress, storageId, error }` with `status` moving through `'pending' | 'uploading' | 'success' | 'error'`.

Besides `items` and `enqueue`, the queue returns aggregate `progress` and `isUploading` refs, `pendingCount`/`activeCount`, and `cancel()` (abort in-flight uploads), `remove(id)`, and `clear()`. Options also take `onItemError(error, item)` and `onComplete(items)` — called once every queued item has settled. Full signatures live in the [API reference](https://nuxt-convex-module.dev/api-reference/reference/client#useuploadqueue).

## `uploadFile()`

The function under both composables, for when you want neither state nor a queue:

```ts
const storageId = await uploadFile({ url, file, onProgress, signal })
```

`url` is a fresh upload URL from your `generateUploadUrl` mutation, `file` a `File` or `Blob`; `onProgress(fraction)` — `0` to `1` — and `signal` (an `AbortSignal`) are optional. It resolves to the `StorageId`. Browser-only — it rejects during SSR — and it rejects with an `AbortError` when the signal fires, or an `Error` naming the status on a non-2xx response, an unexpected body, or a network failure.

## `useStorageUrl()`

Resolve a stored file's served URL (reactive to the id):

```ts
const { storageId } = useUpload(api.files.generateUploadUrl)
const url = useStorageUrl(api.files.getUrl, storageId)
```

The id may be a plain value, ref, computed, or getter; while it is `null`/`undefined` the query is skipped automatically — no `'skip'` juggling. The return is a `ComputedRef<string | null | undefined>`: `undefined` while loading or skipped, `null` when the file no longer exists, the URL once resolved. Templates unwrap it (`<img v-if="url" :src="url">`); in script use `url.value`.

## `<ConvexImage>`

The `<img v-if="url">` above as a component — `useStorageUrl` inside, every other attribute on the `<img>`, and a slot for each of the two other states:

```vue
<ConvexImage :get-url="api.files.getUrl" :storage-id="doc.imageId" alt="Cover" class="rounded" loading="lazy">
  <div class="skeleton" />
  <template #missing><div class="placeholder" /></template>
</ConvexImage>
```

The default slot renders while the URL loads or `storageId` is empty; `missing` renders when the query returns `null`. `getUrl` is read once, at setup, like `useStorageUrl`'s argument.

<callout icon="i-lucide-shield">

With [`nuxt-security`](https://nuxt-convex-module.dev/getting-started/security#content-security-policy) installed, the module's Convex-aware CSP allows Convex-served storage on `img-src` / `media-src` — in `nuxt dev` as well as production builds, from the deployment and `.site` origins. It is skipped if you set `convex.security: false`, `security: false`, or disable `contentSecurityPolicy` yourself.

Uploads go straight from the browser to Convex, so they never pass through Nitro and no server-side request-size limit applies to them — enforce size and content-type limits in your Convex function. See [file storage security](https://nuxt-convex-module.dev/getting-started/security#file-storage).

</callout>


## Sitemap

See the full [sitemap](https://nuxt-convex-module.dev/sitemap.md) for all pages.
