
# Presets & Caching

Two concerns that sit behind the [resize](/en/docs/image-transforms) and [format](/en/docs/image-formats) parameters: giving a recurring combination a name, and making sure the second request for it costs nothing.

## Named Presets

A preset bundles `width`, `height`, `fit`, `crop`, `quality` and `format` under one name. It is operator configuration — a JSON object in an environment variable, not app schema — because the right thumbnail size is a property of the deployment, not of the business domain.

```bash
STORAGE_TRANSFORM_PRESETS='{
  "thumbnail": { "width": 150, "height": 150, "fit": "cover" },
  "preview":   { "width": 600, "quality": 80 },
  "avatar":    { "width": 64, "height": 64, "fit": "cover", "crop": "attention" }
}'
```

```http
GET /api/buckets/photos/files/{key}?preset=avatar
GET /api/buckets/photos/files/{key}?preset=thumbnail&quality=95
```

| Situation                             | Result                                   |
| ------------------------------------- | ---------------------------------------- |
| Known preset                          | Its fields become the transform.         |
| Explicit parameter alongside a preset | The explicit value wins, field by field. |
| Unknown preset name                   | `400`                                    |
| `?preset=` with none configured       | `400`, with a message saying so.         |

Overrides are per-field, not all-or-nothing: `?preset=thumbnail&quality=95` keeps the preset's 150×150 `cover` and changes only the quality.

Preset names must be alphanumeric with single hyphens (`thumbnail`, `hero-banner`), which keeps them safe to put in a URL. Malformed JSON, a non-object value, or an illegal name **fails the server at startup** rather than at the first request — a typo in this variable is found on deploy, not by a user.

## Caching

A transform runs once per distinct combination. The result is held in a process-local LRU cache keyed by the storage key, the transform parameters, and the negotiated format together.

| Mechanism       | Behaviour                                                            |
| --------------- | -------------------------------------------------------------------- |
| `Cache-Control` | `public, max-age=31536000, immutable` — one year, never revalidated. |
| `ETag`          | Derived from the source file plus the transform parameters.          |
| `If-None-Match` | A match returns `304 Not Modified`.                                  |
| Server cache    | In-memory LRU, evicting least-recently-used entries when it is full. |
| Invalidation    | Deleting a file evicts every transform derived from its key.         |

| Variable                           | Default | Meaning                      |
| ---------------------------------- | ------- | ---------------------------- |
| `STORAGE_TRANSFORM_CACHE_MAX_SIZE` | `256`   | Cache ceiling, in megabytes. |

An entry larger than the whole cap is served but never retained — it would evict everything else to fit.

:::callout
**The cache lives in the process, not on disk.** It is empty after every restart and it is not shared between instances, so a multi-process deployment holds one copy per process and warms each independently. That is fine because the CDN-facing `Cache-Control` is a year: the browser and any edge cache absorb the repeat traffic, and this cache only has to absorb the first request per process.
:::

The one-year `immutable` header is safe precisely because keys are content-addressed — a stored key is `<uuid>-<filename>`, so a replaced file is a new key and a new URL. Nothing is ever served stale under a URL whose content changed, because that situation cannot arise.

## Originals Are Never Touched

| Guarantee                    | Behaviour                                                       |
| ---------------------------- | --------------------------------------------------------------- |
| Source bytes                 | Byte-identical after any number of transform requests.          |
| A request with no parameters | The original, subject only to `Accept` negotiation.             |
| `?format=origin`             | The original bytes verbatim, negotiation bypassed.              |
| Clearing the cache           | Discards derived variants only; originals stay fully available. |
| Non-image files              | Transform parameters answer `400` — nothing is attempted.       |

Transforms are derived, disposable, and reproducible: the cache can be dropped at any moment and the next request rebuilds it. Admins can do exactly that with `DELETE /api/admin/storage/transform-cache`.

Recognised image types are those Sovrium can infer from the key's extension: PNG, JPEG, GIF, WebP, AVIF, SVG and ICO. SVG passes through rather than being rasterised, and is always served as an attachment — see [Upload Security](/en/docs/file-security).

## Related Pages

- [Resize & Crop](/en/docs/image-transforms) — the parameters a preset bundles.
- [Format & Quality](/en/docs/image-formats) — format negotiation and compression.
- [File Operations](/en/docs/file-operations) — the endpoint serving cached results.
- [Lifecycle & Quotas](/en/docs/file-lifecycle) — the originals a transform reads from.
- [Environment Variables: Services](/en/docs/env-vars-services) — preset and cache configuration.
