Skip to main content

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

ts
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

  • generateUploadUrl requires identity.
  • attachFile loads the document and verifies edit access.
  • The document stores the storage ID.
  • getUrl checks 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 attachFile runs, 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 progress and a cancel action while uploading is true;
  • an attachment failure (uploadError.phase === 'complete') separately from an upload failure;
  • previewUrl only 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.