I'm building an ecommerce chatbot that helps visitors find products in inventory. Customers may upload photos of items they're looking for, and the chatbot will return product suggestions. I plan to store the uploaded images in a private Cloudflare R2 bucket and need to decide how to display them in the conversation history.
One option is to use caching or public object URLs, which could reduce storage reads. However, each image will probably only be viewed a couple of times, and anyone with the URL could access the file until it is deleted. The other option is to generate presigned R2 URLs that expire after a short period, though that means requests will go directly to R2.
What approach would be best for this use case? I'm also open to other storage solutions if they offer a better security or cost tradeoff.
2 Answers
Treat the photos as private chat data rather than public files. For uploads, issue a temporary write URL and save only the resulting object key. For downloads, check chat access and mint a temporary read URL when the conversation is loaded.
Also add a lifecycle rule for the bucket or an image-specific prefix. Visitors may abandon a chat or cart, so automatic deletion after a reasonable retention period prevents unused photos from accumulating. The expiration policy and access checks matter more than caching, since any valid signed URL can still be forwarded.
Keep the bucket private and use short-lived presigned GET URLs. Store the object key alongside the chat message, but don’t store the signed URL because it will eventually expire. When a user reopens a conversation, first verify that they’re allowed to access that chat, then generate a fresh URL.
Caching probably isn’t worthwhile if each image is only viewed once or twice. A presigned URL is still a bearer token while it’s valid, so use a short expiration—minutes rather than several days where practical—and avoid exposing it in logs or analytics. If you need immediate revocation or authorization on every request, serve the image through an authenticated application or Worker instead.
This sounds like the ideal process. Thanks for the detailed explanation!

The bucket lifecycle rule makes sense too. I hadn’t considered automatic cleanup.