Upload Files
Upload one file to Convex storage with progress, cancellation, and reactive state.
File upload has two network steps: request an upload URL through a mutation, then upload the bytes directly to Convex storage.
Backend
import { mutation } from './_generated/server'
export const generateUploadUrl = mutation({
args: {},
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity()
if (!identity) throw new Error('Unauthenticated')
return await ctx.storage.generateUploadUrl()
},
})Component
<script setup lang="ts">
import { api } from '#convex/api'
const {
upload,
status,
pending,
progress,
error,
data: storageId,
cancel,
} = useConvexFileUpload(api.files.generateUploadUrl, {
maxSize: 5 * 1024 * 1024,
allowedTypes: ['image/*', 'application/pdf'],
})
async function chooseFile(event: Event) {
const input = event.target as HTMLInputElement
const file = input.files?.[0]
if (file) await upload(file)
}
</script>
<template>
<input type="file" :disabled="pending" @change="chooseFile" />
<progress v-if="pending" :value="progress.percent" max="100" />
<button v-if="pending" @click="cancel">Cancel</button>
<p v-if="error">{{ error.message }}</p>
<p v-if="storageId">Uploaded as {{ storageId }}</p>
</template>State
status is idle, pending, success, or error. data contains the last branded Convex storage ID. progress exposes loaded, total, and percent. error is undefined until an upload fails. cancel() aborts an active XHR and resets local state.
One composable instance accepts one active upload. A second call while pending rejects instead of interleaving shared progress state.
Validation boundary
maxSize and allowedTypes provide immediate browser feedback. File name and MIME type are client-controlled. Validate product policy again before publishing or processing the file.
Save metadata
The upload only creates a storage object. Store its ID in a product document through a mutation:
const storageId = await upload(file)
await createDocument({ title, storageId })Plan cleanup if upload succeeds and metadata creation fails.
Multiple files are an application workflow
The library keeps the universal one-file primitive small. If a product needs multiple files, own the item list, scheduling policy, retry UX, and persistence in application code.
This example creates three upload workers during setup and records durable results separately from each worker's transient progress:
import type { Id } from '~/convex/_generated/dataModel'
const workers = [
useConvexFileUpload(api.files.generateUploadUrl),
useConvexFileUpload(api.files.generateUploadUrl),
useConvexFileUpload(api.files.generateUploadUrl),
]
const items = ref<
Array<{
file: File
status: 'queued' | 'uploading' | 'success' | 'error'
storageId?: Id<'_storage'>
error?: unknown
}>
>([])
async function uploadFiles(files: File[]) {
items.value = files.map((file) => ({ file, status: 'queued' }))
let next = 0
await Promise.all(
workers.map(async (worker) => {
while (next < items.value.length) {
const item = items.value[next++]!
item.status = 'uploading'
try {
item.storageId = await worker.upload(item.file)
item.status = 'success'
} catch (error) {
item.error = error
item.status = 'error'
}
}
}),
)
}Choose the worker count from product and network requirements. Add retries, cancellation mapping, resumability, or server-side job records only when the workflow needs them.