File upload form
Upload, validate, attach, preview, and delete a private file.
Result
An authenticated user uploads a bounded image, attaches its storage ID to a document, sees progress and preview, and can delete it through the owning document.
Component flow
import type { UploadCompleteContext } from '@lupinum/better-convex-nuxt'
const props = defineProps<{ documentId: Id<'documents'> }>()
const {
upload,
data: uploaded,
pending: uploading,
progress,
error: uploadError,
cancel,
} = useConvexFileUpload(api.files.generateUploadUrl, {
maxSize: 5 * 1024 * 1024,
allowedTypes: ['image/jpeg', 'image/png', 'image/webp'],
complete: (
op,
{ storageId, context: documentId }: UploadCompleteContext<string, Id<'documents'>>,
) => op.mutation(api.documents.attachFile, { documentId, storageId }),
})
async function submit(file: File) {
await upload(file, {}, { context: props.documentId }).catch(() => undefined)
}
const previewArgs = computed(() =>
uploaded.value ? { storageId: uploaded.value.storageId } : 'skip',
)
const { data: previewUrl } = useConvexQuery(api.files.getUrl, previewArgs, { auth: 'required' })complete attaches the file as part of the upload, for the same signed-in user. The document ID travels with the call as context, so a change of props.documentId during the upload cannot attach the file to another document. pending stays true until the attachment finishes, and uploadError reports a failure of any step. uploadError.phase tells which step failed: prepare, upload, or complete.
Backend rules
generateUploadUrlrequires identity.attachFileloads the document and verifies edit access.- The document stores the storage ID.
getUrlchecks read access before resolving a private URL.- Deletion accepts a document ID, checks ownership, deletes storage, then clears metadata.
- A scheduled cleanup deletes storage objects that no document refers to after a time limit. An upload can store the file and then fail before
attachFileruns, for example when the user signs out.
Presentation
Render:
- selected file name and client preflight errors from
uploadError(FILE_TOO_LARGE,FILE_TYPE_NOT_ALLOWED); - upload
progressand acancelaction whileuploadingistrue; - an attachment failure (
uploadError.phase === 'complete') separately from an upload failure; previewUrlonly after authorized URL resolution;- deletion pending and failure states.
Verify
- Oversized and disallowed browser files fail before upload.
- Unauthorized metadata attachment fails in Convex.
- A failed attachment leaves a file that the scheduled cleanup removes.
- Another user cannot resolve or delete the file.
- Component unmount aborts an active upload, which rejects with
CANCELLED.