Skip to main content
Files go to the local filesystem, an S3 bucket or Google Cloud Storage, behind the same interface. Switching between them is a change in .env rather than a change in your code.
.env
When STORAGE_BACKEND is unset, setting S3_BUCKET selects s3. Otherwise the app’s STORAGE_BACKEND_FALLBACK applies, then local. See automatic backends.

S3 and S3-compatible storage

The s3 backend works with AWS S3 and with services that speak its API, such as MinIO, Garage, Cloudflare R2 and Hetzner Object Storage. It needs the s3 extra:
.env
Buckets are treated as private. upload() and get_url() return a presigned GET URL that expires after S3_URL_EXPIRES seconds. If the bucket is public, or sits behind a CDN, set S3_PUBLIC_URL and they return S3_PUBLIC_URL/<key> instead. With S3_ENDPOINT set, Feather tells boto3 to send and check request checksums only when an operation requires them. boto3 1.36 turned them on for every request, and several S3-compatible services reject those requests. Object keys are checked before any request is made: empty keys, a leading /, . and .. segments, backslashes and control characters raise StorageError.

GCS credentials

Recommended for deployment, since it fits on one line.
.env

Usage

In a route

Upload safety

LocalStorage serves files from static/, where the browser takes the content type from the extension. Script-capable extensions are refused by default, so an uploaded page cannot run on your origin. S3Storage applies the same lists, since a bucket served from one of your domains has the same problem. Setting the block list replaces it. The allow-list wins over the block list, which is how you permit SVG when you need it. An extension says nothing about content. When an upload has to be a particular type, check its first bytes as well: a PDF starts with %PDF-, and a renamed HTML file does not.

Private files, and showing a PDF in the page

A signed URL works for anyone who holds it until it expires. For files that belong to one person, stream them through a route that checks access on every request instead:
To show that PDF in an <iframe> on your own page, allow same-origin framing. The security headers default to frame-ancestors 'none', which blocks your own iframe as well: