File Validation and Deletion
Enforce file policy, prevent orphans, and delete storage with product metadata safely.
A secure file workflow covers the storage object and the product document that references it.
Validation stages
| Stage | Purpose |
|---|---|
| Browser size/type check | Fast UX feedback |
| Authorized upload-URL mutation | Decide who may begin an upload |
| Server/content validation | Verify actual bytes and product policy |
| Metadata mutation | Associate storage ID with an authorized resource |
Never trust file extension or browser MIME type as the security decision.
Prevent orphans
An upload can succeed before metadata creation fails. Choose an explicit strategy:
- delete the new storage object in compensation;
- create a pending upload record before bytes are sent;
- run a bounded cleanup job for unattached uploads.
The module does not own this product lifecycle.
Authorized deletion
export const removeFile = mutation({
args: { documentId: v.id('documents') },
handler: async (ctx, args) => {
const identity = await requireIdentity(ctx)
const document = await ctx.db.get(args.documentId)
if (!document || document.ownerId !== identity.subject) {
throw new ConvexError({ code: 'DOCUMENT_NOT_FOUND' })
}
if (document.storageId) {
await ctx.storage.delete(document.storageId)
}
await ctx.db.patch(document._id, { storageId: undefined })
},
})Authorize by the product document, not by accepting an arbitrary storage ID from the browser.
Processing untrusted files
Use a controlled action or external scanning service for formats that require inspection. Record processing status in Convex so the UI can observe pending, accepted, or rejected state with a query.
Do not publish the storage URL until validation finishes when the product's threat model requires quarantine.