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 subscribe to pending, accepted, or rejected state.
Do not publish the storage URL until validation finishes when the product's threat model requires quarantine.