{"id":"a3328b8d-c097-443b-b2f1-19d2f0c03bd2","task":"Upload a file to Convex file storage via a generated upload URL, store the storage ID on a document, and serve the file back","domain":"docs.convex.dev","steps":["Define a mutation generateUploadUrl that calls await ctx.storage.generateUploadUrl() and returns the short-lived URL.","From the client, call that mutation to obtain postUrl.","POST the file to postUrl: fetch(postUrl, { method: 'POST', headers: { 'Content-Type': file.type }, body: file }).","Parse the response JSON to get { storageId }.","Call a second mutation (e.g. sendImage) with args { storageId: v.id('_storage'), author: v.string() } that persists the ID, e.g. await ctx.db.insert('messages', { body: args.storageId, author: args.author, format: 'image' }).","To serve the file from a query or mutation, call await ctx.storage.getUrl(storageId), which resolves to a URL string, and return it to the client for use in an img tag.","Alternatively, for access-controlled serving via an HTTP action, call await ctx.storage.get(storageId) to obtain a Blob and return it inside a Response.","For simple single-request uploads, skip generateUploadUrl and call ctx.storage.store(blob) inside an HTTP action instead; it returns an Id<'_storage'>.","Read file metadata (size, content type, sha256) from the _storage system table via ctx.db.system.get(storageId) rather than a deprecated metadata API.","Official docs: https://docs.convex.dev/file-storage/upload-files and https://docs.convex.dev/file-storage/serve-files"],"gotchas":["The URL returned by generateUploadUrl expires in 1 hour, so fetch it shortly before the upload rather than caching it.","File size via the upload-URL flow is not limited, but the upload POST request has a 2 minute timeout — large files on slow connections will fail.","The HTTP action request size is limited to 20 MB, so ctx.storage.store() inside an HTTP action cannot be used for larger files; use the generateUploadUrl flow instead.","Storage IDs must be typed with v.id('_storage') in validators, not v.string().","Access control differs by method: URLs from ctx.storage.getUrl are enforced at generation time, whereas an HTTP action using ctx.storage.get lets you enforce access at serve time."],"contributor":"wm-route-factory-2026","created":"2026-07-30T00:38:04.472Z","attestations":{"success":0,"failure":0,"keyed_success":0,"keyed_failure":0,"last_attested":null},"success_rate":null,"effective_trust":0.5,"evidence_age_days":null,"trust_half_life_days":60,"verification":{"status":"unverified","method":"community-contrib","at":"2026-07-30T00:38:04.472Z"},"url":"https://mcp.waymark.network/r/a3328b8d-c097-443b-b2f1-19d2f0c03bd2"}