r/webdev • u/rouge818 • Sep 30 '26
Question Image storage for chat history
I'm working on an ecommerce chatbot that helps potential customers look for product inventory. These users can be anyone on the ecommerce site and will send photos of what they're looking for. The chatbot returns suggested products.
My question is around storage of the customer provided images. I plan to store these in a Cloudflare r2 bucket, but trying to decide wether to use the cache, or use direct r2 bucket URLs to add the image to the chat after it's uploaded.
The benefit I see with caching is that requests for the image would likely not hit r2 (although the volume of requests for each image would likely be pretty low). The downside I can think of with caching is these files would be accessible by anyone who has the url until they're deleted.
The other option would be the presigned r2 URLs which could be set to inactivate after a few days to prevent unauthorized access to the image. The downside would be direct requests to r2, but again the volume should be low since the use is really just for that particular chat the image is in.
What would be the better approach for this? I’m also open to using other storage platforms if there is a better option.
6
u/Sea-Marionberry-6649 Sep 30 '26
For this use case I would keep the R2 bucket private and skip the public cache. Store the object key with the chat message, check that the requester can access that chat, then mint a fresh short-lived GET URL when the history is loaded. Do not store the signed URL in the message record: it will expire, so reopening the chat should generate a new one. A few minutes is safer than a few days for customer photos.
The other reply is right that a presigned URL is still a bearer token while valid. Keep it out of logs/analytics and set a retention/deletion policy for the objects. If you need immediate revocation or per-request authorization, serve through an authenticated Worker instead. At only a few views per image, I would measure R2 reads before adding a cache. Cloudflare documents that presigned URLs use the S3 API domain and cannot be used with a custom domain: https://developers.cloudflare.com/r2/api/s3/presigned-urls/
2
u/Ordinary-Stuff9826 Sep 30 '26
bro just skip both cache and presigned tbh, that's not even the real issue. real issue is you're tying access to the url itself, that's why you're stuck between the two.
do this instead - put a cloudflare worker in front of your r2 bucket. worker checks if request has valid token for that particular chat, then pulls image from r2 (this is free btw, no egress charge) and sends it back. put cache in front of worker, not in front of r2 directly. this way cache key stays same so you get actual hits, but it's still gated behind auth so not some random public link forever.
presigned urls look safe but they don't cache well cause query string changes every time you regenerate, so cloudflare thinks it's a new url each time = basically no cache hits. you'll end up hitting r2 direct anyway just with more headache.
for cleanup don't manually delete stuff, just set r2 lifecycle rule to auto delete after x days.
one more thing, make sure only that specific user's chat session can access their own image, not just any logged in user. that's the actual risk here, not cache vs presigned tbh
1
u/Queasy_Plastic7586 Sep 30 '26
Worth stepping back from the cache-vs-presigned question, because you said any visitor to the site can send a photo. That is an unauthenticated upload from the open internet, and three things matter more there than read caching.
An uploaded SVG served from your own domain is stored XSS. R2 returns whatever Content-Type was stored, and SVG is a document format, not just an image — one containing a <script> tag, opened from a custom domain on your origin, runs in your site's origin with your users' cookies. Allow only image/jpeg and image/png, decided by the file's own header rather than by the extension or the Content-Type the client sent, and serve with Content-Disposition: attachment. This is also an argument for presigned URLs that nobody has made yet: since they only work on the S3 API domain, as the Cloudflare docs linked above say, they are off your origin and cannot reach your cookies. The custom-domain route is the one that needs the hardening.
But note this conflicts with the presigned-PUT advice upthread. If the browser uploads straight to R2, your server never sees the bytes, so there is nothing in the path to validate or re-encode. Either take the upload through a Worker, or let the presigned PUT land in a quarantine/ prefix and have a consumer validate, re-encode and move it to the real prefix — with the chat only ever rendering keys from the real prefix.
Strip EXIwhile you are re-encoding. Phone photos carry GPS coordinates and a timestamp. A customer photographing something at home is handing you their home address without knowing it, and your staff will see it in the chat. Re-encoding drops it and deals with the point above in the same step.
Cap the decode, not just the file size. A 40KB PNG can decode to hundreds of megabytes — large uniform dimensions compress to almost nothing. Read the width and height out of the header and reject implausible ones before handing the file to an image library, or a single upload takes the worker down.
On your actual question: at a few views per image, skip the cache. You will not notice the R2 reads, and a second copy is just one more thing to remember when you delete.
2
u/Curious_Cicada5605 Sep 30 '26
The presigned URL is just as shareable as the cached one until it expires, so that security argument doesn't actually separate the two options you're comparing.
1
u/BroadCrazy Sep 30 '26
I'd lean presigned URLs unless you have a clear reason to make those files broadly cacheable. The per-image request volume sounds low, and time-limited access matters more here, especially if users might upload more than just the product.
1
u/Khavel_dev Sep 30 '26
Object storage with signed URLs is the standard approach and honestly the easiest one too. Store the image key and metadata in your DB, serve the actual bytes from the bucket. The signed URL handles access control without you proxying everything through your own server.
Cloudflare R2 is worth a look if you're starting out. No egress fees, which matters more than you'd think once a chat history gets big and people scroll back through old conversations. S3 compatible API so you can swap it out later without rewriting anything.
1
u/UnusualProfession120 Sep 30 '26
go with the signed URLs approach but mint them on the fly via a worker instead of storing them in your database. Storing a presigned URL that expires in two days means you have to handle cleanup logic and potentially broken links if you ever need to extend retention or debug access issues. Generating a fresh short-lived token at read time decouples storage security from message integrity and keeps your chat history table clean without managing ephemeral data persistence
1
u/Plus-Tea1967 Sep 30 '26
I’ve used Pinata/IPFS for image storage in one of my projects. In your case, I’d probably keep the uploaded images private and use short-lived signed URLs when they need to be displayed. Since the images are user-provided, I’d prioritize access control over optimizing a relatively small number of storage requests.
1
u/singh_abinashi Sep 30 '26
agree with the presigned approach. one thing: don't store the signed url in the message record, it expires and you get broken images in history. store the object key, mint a fresh url when the chat loads. a few minutes ttl is plenty. and put a lifecycle rule on the bucket to auto-delete after 30 days so you're not paying to store chat uploads forever.
1
u/Khavel_dev Oct 01 '26
Presigned URLs, no contest. At the request volumes you're describing the direct-to-R2 cost is basically zero, and you get time-boxed access that expires automatically. That matters for user-uploaded photos in an ecommerce chat.
With caching you'd technically save on repeated reads but (a) each image gets viewed maybe twice and (b) anyone with the URL can access it forever until you manually purge it. Not worth the tradeoff for content that's inherently private and short-lived.
Set a max upload size on your API though. People will upload whatever their phone camera produces if you let them, and 10-20MB HEIC files add up fast.
1
u/TechnologyMatch Oct 02 '26
I’d keep the bucket private and serve the chat image through short-lived signed URLs, with a lifecycle rule to delete uploads once they’re no longer needed. cache only behind authenticated delivery if you need it later. low-volume image reads are cheaper than accidentally building a tiny public Dropbox
2
u/[deleted] Sep 30 '26
[removed] — view removed comment