Radondocs

Signed URLs

Public vs. signed URLs, expiry windows, presigned PUT uploads, forced downloads, and the per-provider support caveats — all through one getUrl() method.

getUrl() turns an object key into a URL. Depending on the options, that's either a plain public URL or a time-limited, cryptographically signed URL — the same method covers both.

storage.getUrl("report.pdf");                          // public URL
await storage.getUrl("report.pdf", { signed: true });  // signed GET, 1-hour default

Public URLs

A public URL is a permanent, unsigned link. It only works if the object lives in a public bucket or is served from a CDN. getUrl(key) (no options) returns it synchronously for most providers.

const url = storage.getUrl("public/logo.png");
// https://my-bucket.s3.us-east-1.amazonaws.com/public/logo.png

If the bucket is private, this URL exists but returns a 403 — reach for a signed URL instead.

Signed URLs

A signed URL grants access to a private object for a limited window, without ever making it public. Radon signs it with your provider credentials (AWS SigV4 for the S3 family, service-SAS for Azure, HMAC tokens for the media CDNs), so it can't be tampered with or extended.

A 15-minute download link
const url = await storage.getUrl("invoices/ada.pdf", {
  signed: true,
  expiresIn: 900, // seconds
});

Hand that URL to a browser and it can GET the object until it expires — then the link is dead.

Expiry

expiresInnumberdefault: 3600

The signed URL's lifetime in seconds. Defaults to 3600 (1 hour). Ignored for public URLs.

SigV4 caps expiry at 7 days

For the S3 family, a presigned URL's maximum lifetime is 604800 seconds (7 days) — the AWS SigV4 limit. Pass a larger expiresIn and the provider rejects the request when the URL is used. For links that must live longer, serve the object publicly or re-sign on demand.

Forcing a download

By default a signed URL displays the object inline. Set download to force the browser to save it instead:

// Save as the object's own filename:
await storage.getUrl("report.pdf", { signed: true, download: true });

// Save under a custom filename:
await storage.getUrl("report.pdf", { signed: true, download: "Q1-Report.pdf" });

This sets response-content-disposition on the signed URL. You can likewise override the response type with responseContentType.

Presigned uploads (PUT)

A presigned PUT URL lets a browser upload directly to your storage provider — the bytes never touch your server. You mint the URL on the backend, send it to the client, and the client PUTs the file straight to it.

Backend: mint the upload URL
const uploadUrl = await storage.getUrl("uploads/user-123/photo.jpg", {
  signed: true,
  method: "PUT",
  expiresIn: 600, // client has 10 minutes to upload
});
Client: PUT the bytes to it
await fetch(uploadUrl, {
  method: "PUT",
  body: file, // a File / Blob
});

PUT requires signed: true

A presigned upload is inherently a signed operation — method: "PUT" implies signed: true. The signature authorizes exactly that one PUT to that one key, for the window you set.

Per-provider support

Signing is not universal. Every adapter declares capabilities.signedUrls and capabilities.presignedUpload; here's the shape of it:

Provider(s)Signed GETPresigned PUTNotes
S3 family, Alibaba OSSYesYesSigV4 (OSS V1 for Alibaba); 7-day cap
Azure BlobYesYesService-SAS URLs
SupabaseYesYesToken-bearing signed GET and signed upload URLs
ImageKitYesNoik-t/ik-s HMAC-signed delivery URLs
CloudinaryYesNos--<sig>-- signed delivery URLs
BunnyConditionalNoNeeds tokenKey and publicUrl set
Local filesystemNoNoNo signing on a filesystem
Vercel BlobNoNoPublic-CDN URLs only
UploadThingNoNoPublic-CDN URLs; upload via its register step

Local, Vercel Blob, and UploadThing can't sign

Calling getUrl(key, { signed: true }) on the local filesystem, Vercel Blob, or UploadThing throws UnsupportedOperationError — none of them have a signing scheme. Local returns a file:// URL (or an HTTP URL if you set publicUrl); Vercel Blob and UploadThing serve everything from a public CDN, so use the public URL from upload() / getUrl().

Bunny signed URLs need two things

Bunny mints signed URLs only when both publicUrl (your pull-zone hostname) and tokenKey (the pull-zone token-authentication key) are configured. Without tokenKey, getUrl(key, { signed: true }) throws; without publicUrl, even the public getUrl(key) throws.

Signed URLs don't fail over

Even with a failover chain, getUrl() always signs against the primary provider — a signed URL is only valid for the store that minted it, so failing over would produce a dead link. See Failover.

On this page