Hi all,
I’m building an image gallery app backed by DigitalOcean Spaces (bucket in fra1, CDN enabled). Public images work great through the CDN endpoint, but now I need to serve private images too.
My current approach is generating presigned URLs with the AWS SDK (v3, S3-compatible client) with a 15-minute expiry. That works, but two things bother me:
What are people actually doing for this? Options I’m considering:
App is a Node.js backend on a basic Droplet, images are 200 KB–2 MB, maybe 50k requests/day. Any patterns or gotchas would be appreciated. Thanks!
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
These answers are provided by our Community. If you find them useful, show some love by clicking the heart. If you run into issues leave a comment, or add your own answer to help others.
Heya,
this is expected behavior, and your two problems are actually the same problem.
Presigned URLs are SigV4, and the signature covers the Host header. You signed for bucket.fra1.digitaloceanspaces.com, so when the request comes through the CDN hostname the signature doesn’t match. The CDN edge doesn’t validate signatures at all — only origin does. There’s no way around this on Spaces currently, the CDN has basically zero config (no custom cache keys, no edge auth).
And even if it did work, you answered your own question #2 — unique query string per user means every request is a cache miss. Presigned + CDN just don’t compose.
That said, 50k req/day is under 1 req/sec. You’re overthinking the origin latency part. If you go with presigned URLs, one trick makes them way better: don’t sign for “now + 15min”, sign for expiry rounded up to the next 15-minute clock boundary. That way everyone asking for the same image inside a window gets the same URL, and browser caching suddenly works. Set Cache-Control on the objects with a max-age shorter than the window and you’ve recovered most of what the CDN was giving you.
The unguessable-key option isn’t security by obscurity, for what it’s worth. A 128-bit random key in the path is exactly as hard to guess as a presigned URL signature — it’s the same entropy, just without an expiry. Slack served attachments this way for years. The actual tradeoffs are: leaked links work forever (until you delete/move the object), and anyone the user forwards a link to can see the image. Also make double sure listing is off on the bucket or the random keys are pointless. If your “private” just means “strangers shouldn’t stumble into these”, this is the easy answer and you keep the CDN.
If “private” means revocable, per-user access — proxy it. But don’t write the caching in Node, just put nginx in front with proxy_cache pointed at Spaces and auth_request to your app for the permission check. At your volume a basic droplet will be bored.
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.
Full documentation for every DigitalOcean product.
The Wave has everything you need to know about building a business, from raising funding to marketing your product.
Scale up as you grow — whether you're running one virtual machine or ten thousand.

From GPU-powered inference and Kubernetes to managed databases and storage, get everything you need to build, scale, and deploy intelligent applications.
