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" 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 storage ID. 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.