Storage URLs
Resolve a Convex storage ID with an ordinary typed query.
Store Convex storage IDs in product data. Resolve a URL with useConvexQuery when the UI needs it.
Backend query
import { query } from './_generated/server'
import { v } from 'convex/values'
export const getUrl = query({
args: { storageId: v.id('_storage') },
handler: async (ctx, args) => {
return await ctx.storage.getUrl(args.storageId)
},
})Add identity and document-ownership checks before returning URLs for private files.
Reactive URL
import type { Id } from '~/convex/_generated/dataModel'
const storageId = ref<Id<'_storage'> | undefined>()
const urlArgs = computed(() => (storageId.value ? { storageId: storageId.value } : 'skip'))
const {
data: imageUrl,
status,
error,
} = useConvexQuery(api.files.getUrl, urlArgs, {
auth: 'required',
})The query is idle until a storage ID exists and runs again when it changes. Use auth: 'required' for private files and keep the authorization check in the Convex query.
Display after upload
const uploadState = useConvexFileUpload(api.files.generateUploadUrl)
const urlArgs = computed(() =>
uploadState.data.value ? { storageId: uploadState.data.value } : 'skip',
)
const { data: imageUrl } = useConvexQuery(api.files.getUrl, urlArgs, { auth: 'required' })<img v-if="imageUrl" :src="imageUrl" alt="Uploaded preview" />undefined means the URL query has no value yet. A null result is a successful response meaning Convex cannot resolve a URL for that storage object.
URL lifetime
Treat the resolved URL as presentation data. Store the branded storage ID as the durable reference and resolve again when needed.