# Cache et images

Bastide tient trois mécanismes qui évitent de refaire le même travail : le cache HTTP de l'edge, qui sert une réponse déjà produite sans solliciter votre application ; la transformation d'images, incluse au forfait ; et le cache distant de construction, compatible Turborepo. Cette page dit comment chacun décide, comment le vider et ce qu'il coûte.

## Le cache HTTP de l'edge

Le cache de l'edge suit un contrat publié : en lisant les en-têtes de votre réponse, vous savez sans essai si elle sera rangée. Rien n'est rangé par défaut ; c'est votre application qui le demande.

### Ce qui se range

Une réponse est rangée quand toutes ces conditions tiennent :

- la requête est un `GET` ou un `HEAD`, sans en-tête `Range`, `Authorization` ni `Upgrade` ;
- l'en-tête qui gouverne porte une durée explicite, sans `private`, `no-cache` ni `no-store` ;
- le statut est 200, 301, 302, 307, 308, 404 ou 410 ;
- la réponse ne pose pas de témoin (`Set-Cookie`) et ne porte pas `Vary: *` ;
- elle pèse 20 Mo au plus, et un 200 a un corps.

L'en-tête qui gouverne est le premier présent parmi `Glowsoft-CDN-Cache-Control`, `CDN-Cache-Control` et `Cache-Control`, et lui seul. `Glowsoft-CDN-Cache-Control` ne parle qu'au cache de la plateforme ; `CDN-Cache-Control` aussi à un CDN que vous placeriez devant ; `Cache-Control` s'adresse d'abord au navigateur.

La durée est `s-maxage`. `max-age` ne compte que dans un en-tête ciblé (`Glowsoft-CDN-Cache-Control` ou `CDN-Cache-Control`), jamais dans `Cache-Control`, où il reste la durée du navigateur. Elle est bornée à un an. `Expires` et les durées heuristiques ne sont pas lus.

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

Cette réponse est gardée une heure par l'edge et une minute par le navigateur.

### Ce qu'il faut savoir

- La clé du cache est la méthode, le nom d'hôte, le chemin et la chaîne de requête. Les témoins de la requête n'en font pas partie : une réponse rangeable ne doit pas en dépendre.
- Un `Vary` autre que `*` range une variante par valeur des en-têtes nommés.
- Le `Cache-Control` envoyé par un visiteur ne contourne rien : un `no-cache` du navigateur ne force pas l'origine.
- Une entrée échue n'est jamais servie : `stale-while-revalidate` et `stale-if-error` ne sont pas encore honorés.
- Un `If-None-Match` qui désigne l'entrée rangée reçoit 304, depuis le cache.
- Un redéploiement de l'environnement purge tout ce que la version précédente avait rangé.
- `Glowsoft-CDN-Cache-Control` et `Glowsoft-Cache-Tag` ne sont jamais renvoyés au visiteur.

### Lire la décision du cache

Chaque réponse porte l'en-tête `Cache-Status` (RFC 9211) : `hit` quand elle vient du cache. Quand le cache ne l'a ni lue ni rangée, `detail=` donne le motif.

| Motif | Cause |
| --- | --- |
| `METHODE` | Ni `GET` ni `HEAD`. |
| `PLAGE` | Un en-tête `Range`. |
| `AUTORISATION` | Un en-tête `Authorization`. |
| `PASSAGE` | Un en-tête `Upgrade` (WebSocket). |
| `STATUT` | Un statut hors de la liste. |
| `TEMOIN` | La réponse pose un témoin. |
| `VARY-ETOILE` | `Vary: *`. |
| `PRIVE` | `private`, `no-cache` ou `no-store`. |
| `SANS-DUREE` | Aucune durée explicite. |
| `TROP-GRAND`, `TROP-GRAND-EN-FLUX` | Plus de 20 Mo, annoncés ou découverts en flux. |
| `CORPS-VIDE` | Un 200 sans corps. |

## Révoquer par étiquette

Votre application peut étiqueter ses réponses par l'en-tête `Glowsoft-Cache-Tag` : des étiquettes séparées par des virgules, 256 caractères au plus chacune. Les étiquettes sont rangées par projet et par environnement : aucune ne traverse un autre locataire.

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

Quand le produit 4512 change, révoquez l'étiquette plutôt que tout le cache. Deux modes existent :

- **invalidate** (recommandé) : l'entrée périmée est servie une dernière fois et revalidée derrière, si bien que le visiteur ne voit pas l'origine ralentir ;
- **delete** : l'entrée est détruite. Employez-le pour retirer un contenu qui ne doit plus être servi.

La révocation prend effet en région en moins de deux secondes, d'après l'API.

### Depuis la console

Ouvrez **Domaines**, puis la carte **Cache de l'edge**. Choisissez l'environnement (la production par défaut), saisissez les étiquettes une par ligne, choisissez le mode, puis **Purger**.

### Par la ligne de commande ou l'API

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

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

Ce corps part en `POST /v1/projects/{projectId}/cache/purge` sur `https://api.bastide.app`. Sans `environmentId`, la révocation vise la production. Cent étiquettes au plus par appel, sans virgule. Le geste demande le rang Développeur et une personne : il n'est ouvert ni aux agents ni aux comptes de service.

## Les transformations d'images

Chaque site sert `/_bastide/image`, qui redimensionne une image et la convertit en AVIF ou en WebP selon ce que le navigateur accepte. Les images du site lui-même sont toujours admises ; une source distante doit figurer dans la liste du projet. Chaque URL est signée : une URL non signée, mal signée ou dont la source n'est pas admise reçoit 403 avant toute lecture.

### Les paramètres

| Paramètre | Sens |
| --- | --- |
| `src` | Un chemin du site (`/images/a.jpg`) ou une URL `https://` d'une source admise. |
| `w`, `h` | Largeur et hauteur, de 1 à 8 192 pixels. |
| `q` | Qualité, de 1 à 100 ; 75 par défaut. |
| `fit` | `cover` (défaut), `contain` ou `fill`. |
| `format` | `avif`, `webp` ou `origin` ; à défaut, négocié par l'en-tête `Accept`. |
| `s` | La signature. Tout autre paramètre est refusé. |

### Signer une URL

La signature est un HMAC-SHA256 de la chaîne canonique, encodé en base64url sans remplissage. La chaîne canonique reprend les paramètres présents parmi `src`, `w`, `h`, `q`, `fit` et `format`, dans cet ordre, sous la forme `nom=valeur` (valeur encodée comme par `encodeURIComponent`), séparés par `&`. La clé est celle que rend le réglage des images, en hexadécimal.

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

const cle = Buffer.from(process.env.IMAGES_CLE_DE_SIGNATURE, "hex");

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

### Régler les sources et la clé

Ouvrez **Domaines**, puis la carte **Images**. Elle montre le compteur de transformations du mois, la génération de la clé et la liste des sources distantes admises, une par ligne (50 au plus, en `https`).

- **Enregistrer et montrer la clé** règle la liste et affiche la clé de signature. Elle ne se montre qu'à ce moment : posez-la aussitôt en variable secrète de votre application. Voir [Environnements et variables](environments.md).
- **Renouveler la clé** en tire une nouvelle : toutes les URL signées sous l'ancienne cessent d'être servies.
- **Optimiser à l'écriture** donne à chaque image déposée au [stockage](storage.md) une variante allégée, servie sans transformation à la lecture. Le réglage est éteint à l'origine.

Les mêmes réglages passent par `image.configure` et se lisent par `image.read`, par l'API (`PUT` et `GET /v1/projects/{projectId}/images`) ou la ligne de commande (`bastide image configure`, `bastide image read`). Régler demande le rang Mainteneur ; lire, le rang Lecteur. Aucun outil MCP ne les porte : la clé est un secret, et aucun outil ne rend de secret.

### Ce qui est compté

Seule une transformation faite est comptée. Une image reprise du cache de l'edge ne l'est pas, ni une image servie telle quelle : un SVG, un GIF ou un WebP animé, une source de moins de 10 Ko, ou un format que la transformation ne lit pas. Ces images portent l'en-tête `X-Image-Diagnostic`, qui dit pourquoi.

Une image transformée est gardée 31 jours par l'edge, dans la portée de l'environnement et de sa version : un redéploiement la périme.

| | Découverte | Standard | Pro | Souverain |
| --- | --- | --- | --- | --- |
| Transformations incluses par mois | 5 000 | 50 000 | 250 000 | 1 000 000 |

Au-delà du volume inclus, l'image est servie telle quelle, sans transformation et sans facturation. Une alerte part à 80 % du volume du mois : l'événement `project.images.threshold`, vers vos [webhooks](webhooks.md) et vos [notifications](notifications.md).

### Limites d'une image

| Limite | Valeur |
| --- | --- |
| Dimensions de la source | 8 192 pixels de côté |
| Poids de la source | 25 Mo |
| Poids de la sortie | 10 Mo |
| Sources distantes par projet | 50 |

Au-delà, la réponse est un 422 qui nomme la limite. Une source illisible donne 502 ; une transformation qui ne répond pas, 503.

## Le cache distant de construction

Turborepo peut déposer et relire ses artefacts dans un cache distant tenu par Bastide, par le protocole `/v8/artifacts`. L'équipe Turborepo est le projet. Pour l'utiliser depuis votre poste ou votre intégration continue :

```sh
export TURBO_API=https://api.bastide.app
export TURBO_TEAM=prj_01JQ8ZK4M7NP2RSTVWXYZ34567
export TURBO_TOKEN=<jeton personnel ou de service>
```

Lire le cache demande le droit de lire le projet ; y déposer, celui de déployer (rang Développeur, portée `deploy:write`). Les constructions de la plateforme reçoivent leur propre jeton, limité au cache de leur projet et retiré dès la fin de la construction. Un artefact qui contient un secret est refusé, et le refus nomme le fichier. Un artefact de plus de 128 Mo n'est pas mis en cache ; Turborepo continue sans lui.

### Rétention et purge

La rétention va de 1 à 30 jours, 7 par défaut ; elle vaut pour les dépôts suivants. La purge prend effet à l'appel : un artefact purgé n'est plus servi.

- Console : **Réglages** du projet, carte **Cache distant Turborepo**. La purge y est totale.
- Agent : l'outil `build_cache_manage`, qui purge aussi par empreinte Turborepo.
- Ligne de commande : `bastide build_cache manage --project-id prj_… --retention-days 14`.

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

Une liste `purge` vide vide tout le cache ; une liste d'empreintes (1 000 au plus) ne retire que celles-ci. La réponse dit combien d'artefacts et d'octets ont été purgés, et ce que le cache tient encore.

## Erreurs fréquentes

| Symptôme ou code | Cause | Remède |
| --- | --- | --- |
| `Cache-Status` avec `detail=SANS-DUREE` | Aucune durée pour l'edge. | Ajouter `s-maxage` à `Cache-Control`, ou poser `Glowsoft-CDN-Cache-Control`. |
| `detail=TEMOIN` | La page pose un témoin de session. | Ne pas poser de témoin sur les réponses publiques. |
| `detail=PRIVE` | `private` ou `no-store` dans l'en-tête qui gouverne. | Retirer la directive, ou cibler l'edge par `Glowsoft-CDN-Cache-Control`. |
| Une page modifiée reste ancienne | Elle est encore dans sa durée de cache. | Révoquer son étiquette, ou redéployer. |
| 403 sur `/_bastide/image` | Signature fausse, paramètre non signé, ou source hors de la liste. | Vérifier la chaîne canonique et la clé ; ajouter la source. |
| Images servies sans transformation | Plafond du mois atteint, ou image exemptée. | Lire `X-Image-Diagnostic` ; changer de palier si le plafond est en cause. |
| `images_non_reglees` | La transformation d'images n'est pas en service sur cette installation. | Contacter le support. |
| `valeur_rendue_une_fois` | Rejeu d'un réglage qui a déjà rendu sa clé. | Régler de nouveau pour obtenir la clé ; elle n'est pas gardée. |

## Voir aussi

- [Routage](routing.md)
- [Domaines](domains.md)
- [Constructions](builds.md)
- [Stockage](storage.md)
- [Limites](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)
