Skip to main content
View as Markdown

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

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.

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.

Built with Sovrium