Skip to main content

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

convex/files.ts
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

ts
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

ts
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' })
vue
<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.