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 defaultPublic 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.pngIf 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.
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: 3600The 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.
const uploadUrl = await storage.getUrl("uploads/user-123/photo.jpg", {
signed: true,
method: "PUT",
expiresIn: 600, // client has 10 minutes to upload
});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 GET | Presigned PUT | Notes |
|---|---|---|---|
| S3 family, Alibaba OSS | Yes | Yes | SigV4 (OSS V1 for Alibaba); 7-day cap |
| Azure Blob | Yes | Yes | Service-SAS URLs |
| Supabase | Yes | Yes | Token-bearing signed GET and signed upload URLs |
| ImageKit | Yes | No | ik-t/ik-s HMAC-signed delivery URLs |
| Cloudinary | Yes | No | s--<sig>-- signed delivery URLs |
| Bunny | Conditional | No | Needs tokenKey and publicUrl set |
| Local filesystem | No | No | No signing on a filesystem |
| Vercel Blob | No | No | Public-CDN URLs only |
| UploadThing | No | No | Public-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.
Uploading
Everything about storing objects with Radon — body vs. path, content-type inference, metadata, cache control, ACLs, and server-side copy.
Resumable uploads
Chunked multipart uploads for large files — the uploadResumable() helper, the low-level multipart primitives, part sizes, and which providers support them.