
# Storage Backends

A [bucket](/en/docs/buckets-overview) 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](/en/docs/signed-urls), and [image transforms](/en/docs/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.

```bash
# Zero-config: nothing set, SQLite database → local files under the data dir
# Explicit local
STORAGE_PROVIDER=local
STORAGE_LOCAL_DIRECTORY=/var/lib/sovrium/uploads
```

```bash
# 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
```

:::callout
**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.
:::

:::callout
**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](/en/docs/buckets-overview) — the buckets that sit on top of a backend.
- [Bucket Permissions](/en/docs/buckets-permissions) — who may read and write.
- [File Operations](/en/docs/file-operations) — the endpoints every backend serves.
- [Signed URLs](/en/docs/signed-urls) — presigned on S3, HMAC-signed elsewhere.
- [Environment Variables: Services](/en/docs/env-vars-services) — the full `STORAGE_*` reference.
- [Ecoconception](/en/docs/ecoconception) — why the zero-dependency default is the default.
