# Cache and images

Bastide runs three mechanisms that avoid doing the same work twice: the edge's HTTP cache, which serves an already produced response without calling your application; image transformation, included in the plan; and the remote build cache, compatible with Turborepo. This page explains how each one decides, how to empty it and what it costs.

## The edge HTTP cache

The edge cache follows a published contract: by reading your response headers, you know without testing whether it will be stored. Nothing is stored by default; your application asks for it.

### What gets stored

A response is stored when all of these hold:

- the request is a `GET` or a `HEAD`, without a `Range`, `Authorization` or `Upgrade` header;
- the governing header carries an explicit lifetime, without `private`, `no-cache` or `no-store`;
- the status is 200, 301, 302, 307, 308, 404 or 410;
- the response sets no cookie (`Set-Cookie`) and does not carry `Vary: *`;
- it weighs 20 MB at most, and a 200 has a body.

The governing header is the first one present among `Glowsoft-CDN-Cache-Control`, `CDN-Cache-Control` and `Cache-Control`, and that one alone. `Glowsoft-CDN-Cache-Control` speaks only to the platform's cache; `CDN-Cache-Control` also to a CDN you might place in front; `Cache-Control` is addressed to the browser first.

The lifetime is `s-maxage`. `max-age` only counts in a targeted header (`Glowsoft-CDN-Cache-Control` or `CDN-Cache-Control`), never in `Cache-Control`, where it remains the browser's lifetime. It is capped at one year. `Expires` and heuristic lifetimes are not read.

```
Cache-Control: public, max-age=60, s-maxage=3600
```

This response is kept for one hour by the edge and one minute by the browser.

### Worth knowing

- The cache key is the method, the host name, the path and the query string. Request cookies are not part of it: a storable response must not depend on them.
- A `Vary` other than `*` stores one variant per value of the named headers.
- A visitor's `Cache-Control` bypasses nothing: a browser's `no-cache` does not force the origin.
- An expired entry is never served: `stale-while-revalidate` and `stale-if-error` are not honoured yet.
- An `If-None-Match` that designates the stored entry receives 304, from the cache.
- Redeploying the environment purges everything the previous version had stored.
- `Glowsoft-CDN-Cache-Control` and `Glowsoft-Cache-Tag` are never returned to the visitor.

### Reading the cache's decision

Every response carries the `Cache-Status` header (RFC 9211): `hit` when it comes from the cache. When the cache neither read nor stored it, `detail=` gives the reason.

| Reason | Cause |
| --- | --- |
| `METHODE` | Neither `GET` nor `HEAD`. |
| `PLAGE` | A `Range` header. |
| `AUTORISATION` | An `Authorization` header. |
| `PASSAGE` | An `Upgrade` header (WebSocket). |
| `STATUT` | A status outside the list. |
| `TEMOIN` | The response sets a cookie. |
| `VARY-ETOILE` | `Vary: *`. |
| `PRIVE` | `private`, `no-cache` or `no-store`. |
| `SANS-DUREE` | No explicit lifetime. |
| `TROP-GRAND`, `TROP-GRAND-EN-FLUX` | Over 20 MB, announced or discovered while streaming. |
| `CORPS-VIDE` | A 200 without a body. |

## Revoking by tag

Your application can tag its responses with the `Glowsoft-Cache-Tag` header: comma-separated tags, 256 characters at most each. Tags are stored per project and per environment: none crosses into another tenant.

```
Glowsoft-Cache-Tag: product:4512, catalogue
```

When product 4512 changes, revoke the tag rather than the whole cache. There are two modes:

- **invalidate** (recommended): the stale entry is served one last time and revalidated behind the scenes, so the visitor does not see the origin slow down;
- **delete**: the entry is destroyed. Use it to take down content that must no longer be served.

Revocation takes effect in the region in under two seconds, according to the API.

### From the console

Open **“Domaines”** (Domains), then the **Cache de l'edge** (Edge cache) card. Choose the environment (production by default), enter the tags one per line, choose the mode, then **Purger** (Purge).

### From the command line or the API

```sh
bastide cache tag revoke --project-id prj_01JQ8ZK4M7NP2RSTVWXYZ34567 --tags '["product:4512","catalogue"]'
```

```json
{
  "projectId": "prj_01JQ8ZK4M7NP2RSTVWXYZ34567",
  "tags": ["product:4512", "catalogue"],
  "mode": "invalidate",
  "idempotencyKey": "purge-product-4512-2026-10-14"
}
```

This body is sent as `POST /v1/projects/{projectId}/cache/purge` on `https://api.bastide.app`. Without `environmentId`, revocation targets production. A hundred tags at most per call, without commas. The action requires the Developer rank and a person: it is open neither to agents nor to service accounts.

## Image transformations

Every site serves `/_bastide/image`, which resizes an image and converts it to AVIF or WebP depending on what the browser accepts. The site's own images are always allowed; a remote source must be on the project's list. Every URL is signed: an unsigned or wrongly signed URL, or one whose source is not allowed, receives 403 before anything is read.

### Parameters

| Parameter | Meaning |
| --- | --- |
| `src` | A path on the site (`/images/a.jpg`) or an `https://` URL from an allowed source. |
| `w`, `h` | Width and height, from 1 to 8,192 pixels. |
| `q` | Quality, from 1 to 100; 75 by default. |
| `fit` | `cover` (default), `contain` or `fill`. |
| `format` | `avif`, `webp` or `origin`; otherwise negotiated from the `Accept` header. |
| `s` | The signature. Any other parameter is refused. |

### Signing a URL

The signature is an HMAC-SHA256 of the canonical string, encoded as base64url without padding. The canonical string takes the parameters present among `src`, `w`, `h`, `q`, `fit` and `format`, in that order, as `name=value` (value encoded as by `encodeURIComponent`), joined by `&`. The key is the one returned when you configure images, in hexadecimal.

```js
import { createHmac } from "node:crypto";

const key = Buffer.from(process.env.IMAGES_SIGNING_KEY, "hex");

export function imageUrl(src, width, quality = 75) {
  const p = new URLSearchParams({ src, w: String(width), q: String(quality) });
  const canonical = ["src", "w", "h", "q", "fit", "format"]
    .filter((n) => p.has(n))
    .map((n) => `${n}=${encodeURIComponent(p.get(n))}`)
    .join("&");
  p.set("s", createHmac("sha256", key).update(canonical).digest("base64url"));
  return `/_bastide/image?${p}`;
}
```

### Configuring sources and the key

Open **“Domaines”**, then the **Images** card. It shows the month's transformation counter, the key generation and the list of allowed remote sources, one per line (50 at most, over `https`).

- **Enregistrer et montrer la clé** (Save and show the key) saves the list and displays the signing key. It is shown only at that moment: set it straight away as a secret variable of your application. See [Environments and variables](environments.md).
- **Renouveler la clé** (Renew the key) draws a new one: every URL signed with the old key stops being served.
- **Optimiser à l'écriture** (Optimise on write) gives each image uploaded to [storage](storage.md) a lighter variant, served without transformation on read. The setting is off initially.

The same settings go through `image.configure` and are read with `image.read`, through the API (`PUT` and `GET /v1/projects/{projectId}/images`) or the command line (`bastide image configure`, `bastide image read`). Configuring requires the Maintainer rank; reading, the Reader rank. No MCP tool carries them: the key is a secret, and no tool returns a secret.

### What is counted

Only a transformation actually performed is counted. An image taken from the edge cache is not, nor is an image served as is: an SVG, an animated GIF or WebP, a source under 10 KB, or a format the transformation cannot read. These images carry the `X-Image-Diagnostic` header, which says why.

A transformed image is kept for 31 days by the edge, within the scope of the environment and its version: redeploying expires it.

| | Discovery | Standard | Pro | Sovereign |
| --- | --- | --- | --- | --- |
| Transformations included per month | 5,000 | 50,000 | 250,000 | 1,000,000 |

Beyond the included volume, the image is served as is, without transformation and without charge. An alert is sent at 80% of the month's volume: the `project.images.threshold` event, to your [webhooks](webhooks.md) and [notifications](notifications.md).

### Limits of an image

| Limit | Value |
| --- | --- |
| Source dimensions | 8,192 pixels per side |
| Source weight | 25 MB |
| Output weight | 10 MB |
| Remote sources per project | 50 |

Beyond these, the response is a 422 naming the limit. An unreadable source gives 502; a transformation that does not answer, 503.

## The remote build cache

Turborepo can store and read back its artefacts in a remote cache kept by Bastide, over the `/v8/artifacts` protocol. The Turborepo team is the project. To use it from your machine or your continuous integration:

```sh
export TURBO_API=https://api.bastide.app
export TURBO_TEAM=prj_01JQ8ZK4M7NP2RSTVWXYZ34567
export TURBO_TOKEN=<personal or service token>
```

Reading the cache requires the right to read the project; storing, the right to deploy (Developer rank, `deploy:write` scope). The platform's own builds receive their own token, limited to their project's cache and withdrawn as soon as the build ends. An artefact containing a secret is refused, and the refusal names the file. An artefact over 128 MB is not cached; Turborepo carries on without it.

### Retention and purge

Retention ranges from 1 to 30 days, 7 by default; it applies to subsequent uploads. A purge takes effect on the call: a purged artefact is no longer served.

- Console: the project's **“Réglages”** (Settings), **Cache distant Turborepo** (Turborepo remote cache) card. The purge there is total.
- Agent: the `build_cache_manage` tool, which can also purge by Turborepo hash.
- Command line: `bastide build_cache manage --project-id prj_… --retention-days 14`.

```json
{
  "projectId": "prj_01JQ8ZK4M7NP2RSTVWXYZ34567",
  "purge": [],
  "idempotencyKey": "purge-remote-cache-2026-10-14"
}
```

An empty `purge` list empties the whole cache; a list of hashes (1,000 at most) removes only those. The response says how many artefacts and bytes were purged, and what the cache still holds.

## Common errors

| Symptom or code | Cause | Remedy |
| --- | --- | --- |
| `Cache-Status` with `detail=SANS-DUREE` | No lifetime for the edge. | Add `s-maxage` to `Cache-Control`, or set `Glowsoft-CDN-Cache-Control`. |
| `detail=TEMOIN` | The page sets a session cookie. | Do not set cookies on public responses. |
| `detail=PRIVE` | `private` or `no-store` in the governing header. | Remove the directive, or target the edge with `Glowsoft-CDN-Cache-Control`. |
| A changed page stays old | It is still within its cache lifetime. | Revoke its tag, or redeploy. |
| 403 on `/_bastide/image` | Wrong signature, unsigned parameter, or source not on the list. | Check the canonical string and the key; add the source. |
| Images served untransformed | The month's cap is reached, or the image is exempt. | Read `X-Image-Diagnostic`; change tier if the cap is the cause. |
| `images_non_reglees` | Image transformation is not in service on this installation. | Contact support. |
| `valeur_rendue_une_fois` | Replay of a configuration call that already returned its key. | Configure again to obtain the key; it is not stored. |

## See also

- [Routing](routing.md)
- [Domains](domains.md)
- [Builds](builds.md)
- [Storage](storage.md)
- [Limits](limits.md)
- [cache.tag.revoke](../reference/operations/cache.tag.revoke.md), [image.configure](../reference/operations/image.configure.md), [build_cache.manage](../reference/operations/build_cache.manage.md) (operation pages, in French)
