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, with 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 and the maximum is 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, which go in parallel and resume cleanly after a network error. 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:
Good fits: marketing images, hero photos, downloadable PDFs, font files, public datasets, and any static asset for a Web Service or Static Site that doesn’t need access control.
Bad fit: anything per-user, like invoices or receipts. Keep those in a private bucket and hand out presigned URLs.
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 on their own and can be issued per user.