Storage Backends
A bucket says how files are organized. The backend says where their bytes end up. It is an operator decision, made entirely with environment variables, and it never appears in the app schema — the same config file runs against local disk in development and against S3 in production.
All three backends support the full surface: upload, download, delete, signed URLs, and image transforms.
| Backend | STORAGE_PROVIDER |
Best for | Scale |
|---|---|---|---|
| Local filesystem | local |
Development, single-node, self-hosted | Disk-bound |
| S3 / S3-compatible | s3 |
Production, multi-node | Unlimited |
| MinIO, R2, Scaleway | s3 + STORAGE_S3_FORCE_PATH_STYLE=true |
Self-hosted or non-AWS object stores | Unlimited |
PostgreSQL bytea |
bytea |
Small files, one-database deploys, no disk | Database-bound |
Choosing a Backend
STORAGE_PROVIDER is not required. Left unset, the backend follows the database dialect:
STORAGE_PROVIDER |
DATABASE_URL |
Resulting backend |
|---|---|---|
s3 |
either | S3. All five STORAGE_S3_* credentials required. |
local |
either | Local filesystem. STORAGE_LOCAL_DIRECTORY is required — no fallback. |
| unset | set (Postgres) | bytea — bytes go in the database you already run. |
| unset | unset (SQLite) | Local filesystem at <data dir>/storage. |
The two unset rows are the zero-config path, and they mirror the SQLite-default database posture: nothing external to provision, nothing to credential. Note the asymmetry — an explicit STORAGE_PROVIDER=local fails at boot without STORAGE_LOCAL_DIRECTORY, while an implicit local picks the default directory for you. The explicit form is treated as a deliberate statement about placement, so a missing directory is a mistake worth surfacing.
# Zero-config: nothing set, SQLite database → local files under the data dir
# Explicit local
STORAGE_PROVIDER=local
STORAGE_LOCAL_DIRECTORY=/var/lib/sovrium/uploads
# S3-compatible (MinIO shown — drop the path-style flag for AWS)
STORAGE_PROVIDER=s3
STORAGE_S3_ENDPOINT=https://minio.example.com
STORAGE_S3_BUCKET=my-app-files
STORAGE_S3_REGION=eu-west-1
STORAGE_S3_ACCESS_KEY_ID=...
STORAGE_S3_SECRET_ACCESS_KEY=...
STORAGE_S3_FORCE_PATH_STYLE=true
Your buckets are prefixes inside one host bucket. Every bucket you declare in buckets[] becomes a path prefix (avatars/, documents/) inside the single object-store bucket named by STORAGE_S3_BUCKET. You do not create one S3 bucket per Sovrium bucket, and you do not need permission to create buckets at all.
The bare S3_* names are deprecated. S3_ENDPOINT, S3_BUCKET and the rest are still read as aliases and log a one-time warning at startup. Rename them to STORAGE_S3_*; the legacy spelling goes away in the next major version.
Signing Across Backends
S3 exposes native presigning, so signed URLs on that backend delegate to the provider. Local and bytea have no such primitive, so Sovrium signs and verifies HMAC tokens itself against its own /signed route. The difference is invisible to callers: the same request produces the same shape of URL on all three, and a URL minted on one backend is never portable to another.
Related Pages
- Buckets Overview — the buckets that sit on top of a backend.
- Bucket Permissions — who may read and write.
- File Operations — the endpoints every backend serves.
- Signed URLs — presigned on S3, HMAC-signed elsewhere.
- Environment Variables: Services — the full
STORAGE_*reference. - Ecoconception — why the zero-dependency default is the default.
Last updated July 27, 2026
This documentation was written with AI, so errors or outdated content are possible. Sovrium is in beta — contributions and corrections are welcome.