---
title: "File storage"
description: "Reactive uploads with progress, cancellation, and queueing over Convex file storage."
canonical_url: "https://nuxt-backend.dev/docs/data/files"
---
# File storage

> Reactive uploads with progress, cancellation, and queueing over Convex file storage.

Convenience helpers for [Convex file storage](https://docs.convex.dev/file-storage). Convex's own React client ships no upload composable — file storage is plain app code (a `generateUploadUrl` mutation, a `POST` to the returned URL, then a query to serve the file). These helpers wrap that flow with reactive progress, cancellation, queueing, and URL resolution, while staying type-safe against your own functions.

They assume your Convex backend exposes the standard storage functions, and every sample imports `api` from [`#backend/api`](https://nuxt-backend.dev/docs/reference/nuxt/import-aliases) — it is not auto-imported:

```ts [backend/files.ts]
import { v } from 'convex/values'
import { mutation, query } from './_generated/server'

// Returns a one-time upload URL. Add auth checks as needed.
export const generateUploadUrl = mutation({
  args: {},
  handler: ctx => ctx.storage.generateUploadUrl(),
})

// Resolve the served URL for a stored file (null if it was deleted).
export const getUrl = query({
  args: { storageId: v.id('_storage') },
  handler: (ctx, { storageId }) => ctx.storage.getUrl(storageId),
})
```

## `useUpload()`

Upload a single file with reactive `isUploading`, `progress` (`0`–`1`), `error`, and `storageId`. `upload()` resolves to the new storage id (or `null` on failure — inspect `error` rather than `try`/`catch`). Uploads are client-only; calling `upload()` during SSR resolves to `null`.

```vue
<script setup lang="ts">
import { api } from '#backend/api'

const saveImage = useMutation(api.images.save)
const { upload, isUploading, progress, error, cancel } = useUpload(
  api.files.generateUploadUrl,
  { onSuccess: storageId => saveImage({ storageId }) },
)

async function onPick(event: Event) {
  const file = (event.target as HTMLInputElement).files?.[0]
  if (file) await upload(file)
}
</script>

<template>
  <input type="file" :disabled="isUploading" @change="onPick">
  <progress v-if="isUploading" :value="progress" />
  <button v-if="isUploading" @click="cancel">Cancel</button>
  <p v-if="error">{{ error.message }}</p>
</template>
```

`onProgress`, `onSuccess(storageId, file)`, and `onError(error, file)` callbacks are supported. `cancel()` aborts the in-flight upload; `reset()` clears `progress`/`error`/`storageId`.

## `useUploadQueue()`

Upload many files with bounded concurrency (default `3`). `enqueue()` accepts a file, an array, or a `FileList` straight from a multi-file `<input>` and starts immediately. Exposes a reactive `items` list (each with its own `status`/`progress`/`storageId`/`error`), plus aggregate `progress`, `isUploading`, `activeCount`, and `pendingCount`.

```vue
<script setup lang="ts">
import { api } from '#backend/api'

const saveImage = useMutation(api.images.save)
const { items, enqueue, progress, isUploading } = useUploadQueue(
  api.files.generateUploadUrl,
  { concurrency: 4, onItemSuccess: storageId => saveImage({ storageId }) },
)
</script>

<template>
  <input type="file" multiple @change="enqueue(($event.target as HTMLInputElement).files)">
  <progress v-if="isUploading" :value="progress" />
  <ul>
    <li v-for="item in items" :key="item.id">
      {{ item.status }} — {{ Math.round(item.progress * 100) }}%
    </li>
  </ul>
</template>
```

`cancel()` aborts all in-flight uploads, `remove(id)` drops one item, and `clear()` empties the queue. `onItemSuccess`, `onItemError`, and `onComplete` callbacks are supported.

## `useStorageUrl()`

Reactively resolve a stored file's served URL via your `getUrl` query. Accepts a reactive storage id and automatically skips the query while the id is `null`/`undefined`.

```vue
<script setup lang="ts">
import { api } from '#backend/api'

const { storageId } = useUpload(api.files.generateUploadUrl)
const url = useStorageUrl(api.files.getUrl, storageId)
</script>

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

Returns a shallow ref of `string | null` once loaded (`null` if the file is gone) or `undefined` while loading/skipped.

## `<StorageImage>`

An `<img>` for a file in storage: it resolves the URL through `useStorageUrl` and renders once the URL is known. Every other attribute (`alt`, `class`, `loading`, `width`) goes onto the `<img>`. The default slot renders while the URL loads or `storage-id` is empty, and the `missing` slot once the file is gone:

```vue
<script setup lang="ts">
import { api } from '#backend/api'

defineProps<{ imageId: string | null }>()
</script>

<template>
  <StorageImage :get-url="api.files.getUrl" :storage-id="imageId" alt="Cover" class="cover">
    <div class="skeleton" />
    <template #missing>
      <div class="placeholder" />
    </template>
  </StorageImage>
</template>
```

`get-url` is read once, at setup; `storage-id` is reactive.

## `uploadFile()`

Low-level promise-based uploader behind the composables, for when you want the bare promise rather than reactive state — an event handler, say. It needs no client of its own, only the upload URL your `generateUploadUrl` mutation mints: from `useBackendClient()` in a component setup, as below, or from whatever client you hold elsewhere. Takes `{ url, file, onProgress?, signal? }` and resolves to the storage id:

```ts
import { api } from '#backend/api'
import { uploadFile, useBackendClient } from '#imports'

const backend = useBackendClient() // in a component setup — elsewhere, use whatever client you have
const url = await backend.mutation(api.files.generateUploadUrl, {})
const storageId = await uploadFile({ url, file })
```


## Sitemap

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