The blog’s audio moved into a Cloudflare R2 bucket this week, and the bucket needed to hand out URLs. The obvious answer was already running on the same account: a sibling service that presigns its links - X-Amz-Expires=86400, one day - and is correct to.
The sibling is an API. It mints a URL inside each response, so the link and the response are born together and die together. A day of validity outlives any client that will ever follow it. The URL is never written down anywhere its expiry can’t see.
Presigning caps at seven days. X-Amz-Expires tops out at 604800 seconds - a ceiling in the SigV4 protocol itself, not a bucket setting. Nobody minds, because nothing in the design expects a signed URL to get old.
The first sketch here was the same shape: private bucket, signed links, nothing public. It died in review in one sentence - the link lands in HTML that outlives it.
Hugo mints the audio URL once, at build, and writes it into every page that carries a player. That HTML is a deploy artifact. It serves unchanged until the next deploy, and nothing schedules the next deploy - a blog ships when there’s something to ship. The longest quiet stretch here in the past three months ran eighteen days. A presigned URL baked in at build would have spent eleven of them dead.
The trap word is “cache”, because the CDN is innocent here. This site’s HTML serves with max-age=0 and revalidates on every request; the edge holds nothing. Strip every cache out of the path and the problem doesn’t move. The deploy artifact itself is the cache - a snapshot of every URL the site knows, invalidated only by the next build. The deploy is the cache.
Same account, same storage API, separate buckets - and presigning is right on one side, structurally dead on the other. Nothing in the tool tells you which side you’re on. The variable is when the URL is minted: request time or build time. The expiry clock starts at mint, and a static site’s clock starts before the first reader arrives. The rule: a URL has to outlive whatever holds it. The sibling’s URLs are held by a response that’s gone in milliseconds. This site’s URLs are held by a deploy that lives until I ship the next one.
A presigned URL is a spoken thing - said once, to one listener, gone. A static site writes everything down.
