Presigned URLs & public access
There are two ways to give someone access to an object without handing them an access key: presigned URLs for time-limited, per-object access, and public buckets for assets you want anyone to read forever.
Presigned URLs
Section titled “Presigned URLs”A presigned URL is a regular HTTPS URL with the request signature baked in as query parameters. Whoever holds the URL can perform exactly one operation on exactly one object until the URL expires.
Typical uses:
- Let a browser upload directly to the bucket — no need to stream bytes through your backend.
- Hand out a time-limited download link for a private file (an invoice, a report, a paid asset).
- Generate a one-time link in an email so the recipient doesn’t need an account.
URLs are short-lived. Default lifetime is 10 minutes, max 24 hours — pick the smallest window that fits your use case.
From the dashboard
Section titled “From the dashboard”In the bucket’s Files tab, click the Share link icon on any file. The dashboard opens a modal with a copy-pasteable presigned download URL. If the bucket is public the modal switches to showing the permanent public URL instead.
From boto3 (Python)
Section titled “From boto3 (Python)”# Generate a presigned download URL — valid for 1 hourdownload_url = s3.generate_presigned_url( "get_object", Params={"Bucket": "user-uploads", "Key": "reports/2026-05.pdf"}, ExpiresIn=3600,)
# Generate a presigned upload URL — valid for 10 minutesupload_url = s3.generate_presigned_url( "put_object", Params={ "Bucket": "user-uploads", "Key": "avatars/u123.png", "ContentType": "image/png", }, ExpiresIn=600,)From @aws-sdk/client-s3 (Node)
Section titled “From @aws-sdk/client-s3 (Node)”import { getSignedUrl } from '@aws-sdk/s3-request-presigner';import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
// Download URL — valid for 1 hourconst downloadUrl = await getSignedUrl( s3, new GetObjectCommand({ Bucket: 'user-uploads', Key: 'reports/2026-05.pdf' }), { expiresIn: 3600 },);
// Upload URL — valid for 10 minutesconst uploadUrl = await getSignedUrl( s3, new PutObjectCommand({ Bucket: 'user-uploads', Key: 'avatars/u123.png', ContentType: 'image/png', }), { expiresIn: 600 },);Browser-side upload pattern
Section titled “Browser-side upload pattern”The recommended flow for a SPA that lets users upload files:
- Browser asks your backend for a presigned PUT URL for a key like
uploads/<user_id>/<uuid>.png. - Backend uses an S3 SDK with the access key (kept on the server!) to generate the signed URL and returns it.
- Browser does a single
fetch(url, { method: 'PUT', body: file })to upload directly to Runsite — bytes never flow through your backend.
// browserconst res = await fetch('/api/upload-url', { method: 'POST', body: JSON.stringify({ name: file.name }) });const { url } = await res.json();
await fetch(url, { method: 'PUT', headers: { 'Content-Type': file.type }, body: file,});Multipart uploads
Section titled “Multipart uploads”Files larger than ~5 MB benefit from multipart uploads — they upload in parallel and resume cleanly on network errors. Every official S3 SDK does this transparently when you use the high-level helpers (upload_file / upload_fileobj in boto3, Upload in @aws-sdk/lib-storage, s3 cp and s3 sync in the AWS CLI).
For browser-side multipart, generate a separate presigned URL for each part. The flow is:
- Backend calls
CreateMultipartUploadand returns theUploadId. - Backend generates a presigned
UploadPartURL for each part. - Browser uploads the parts in parallel.
- Backend calls
CompleteMultipartUploadwith the resultingETags.
Public buckets
Section titled “Public buckets”When you flip a bucket to Public access in Settings, every object in it becomes readable anonymously over HTTPS:
https://s3.runsite.app/<bucket>/<key>https://<bucket>.s3.runsite.app/<key>That’s it — drop these URLs into <img> tags, CSS, <a href>, marketing pages, RSS feeds, anywhere.
When to use public buckets:
- ✅ Marketing images, hero photos, downloadable PDFs, font files, public datasets.
- ✅ Static assets for a Web Service or Static Site that don’t need access control.
- ❌ User-private data — invoices, receipts, anything per-user. Use a private bucket and presigned URLs instead.
Switching a bucket from private to public exposes every existing object. Audit the contents first.
CORS for browser uploads
Section titled “CORS for browser uploads”If your browser uploads (presigned PUT, multipart) fail with a CORS error, the bucket needs a CORS rule that permits your origin and the request method. Configure it from the bucket’s Settings tab — the configuration shape matches the standard S3 CORSConfiguration:
{ "CORSRules": [ { "AllowedOrigins": ["https://app.example.com"], "AllowedMethods": ["GET", "PUT", "POST", "HEAD"], "AllowedHeaders": ["*"], "ExposeHeaders": ["ETag"], "MaxAgeSeconds": 3000 } ]}Server-to-server traffic (your backend, CI, scripts) does not need CORS — only browser code does.
Rule of thumb
Use public buckets for things you would put on a CDN. Use presigned URLs for everything else — they keep the secret on the server, expire automatically and can be issued per-user.