# Données : collections, fichiers, réglages

> Où vivent les données de votre app (chez l’organisation), comment les déclarer, les lire et les écrire, et ce qu’il en advient à la désinstallation.
> Catégorie : Référence · mis à jour le 2026-09-04 · plateforme v1

## Le principe

Une app Capibara ne possède pas de base de données : **ses données vivent dans
l'organisation qui l'a installée**, cloisonnées par app. Trois familles, toutes
servies par `ctx` (code hébergé) :

| Famille | `ctx` | Capability | Pour quoi |
|---|---|---|---|
| **Collections** | `ctx.data.<nom>` | `data.collections` | des enregistrements typés, déclarés au manifeste (scores, favoris, tickets…) |
| **Clé/valeur** | `ctx.kv` | `storage.tenant.ro` / `.rw` | curseurs, caches, petits réglages (32 Ko / 200 clés) |
| **Fichiers privés** | `ctx.files` | `files.private` | pièces jointes, exports, images générées (≤ 5 Mo) |

Et une quatrième, saisie par un **administrateur** de l'organisation, jamais par
votre code : les **réglages** (`settings` du manifeste → `ctx.install.config`),
avec des champs `secret` chiffrés au repos.

Conséquences, toutes voulues : la donnée est **supprimée avec l'organisation**,
**incluse dans son export RGPD**, **conservée 30 jours** après une
désinstallation (réinstaller la retrouve) puis **purgée** par la plateforme —
l'admin peut aussi purger tout de suite depuis la fiche de l'app. Vous ne
gérez ni migration, ni sauvegarde, ni DDL : le manifeste est le contrat.

## Déclarer des collections

```json
{
  "capabilities": ["data.collections"],
  "collections": {
    "favorites": { "fields": { "city": "string", "addedAt": "date" } },
    "scores":    { "fields": { "partyId": "string", "value": "number", "label": "string", "meta": "json" }, "indexes": ["partyId"] }
  }
}
```

- Types de champ : `string` (≤ 20 000 caractères), `number`, `boolean`,
  `date` (ISO 8601, normalisée à l'écriture), `json` (valeur libre ≤ 16 Ko).
- Au plus **10 collections** et **30 champs** par collection ; noms =
  identifiants simples (`a-z` puis lettres/chiffres/`_`). Le nom `$files`
  est réservé.
- `indexes` est documentaire : les égalités sont déjà indexées (GIN).
- Requiert `main` : seules les apps hébergées y accèdent — un [backend
  externe](/dev/docs/backend-externe) n'a ni collections, ni KV, ni fichiers
  (le manifeste les refuse sans `main`).

Toute écriture est **validée contre la déclaration** : un champ non déclaré,
un type faux ou un enregistrement > 64 Ko sont refusés avec un message qui
nomme le champ. Un `null` est accepté partout (champ vide).

## Lire et écrire — `ctx.data`

```ts
// Créer ou remplacer (clé fournie) — la clé est générée (« rec_… ») si absente.
const fav = await ctx.data.favorites.set({ city: 'Lyon', addedAt: new Date().toISOString() }, 'lyon');

// Lire une ligne par sa clé.
const row = await ctx.data.favorites.get('lyon');   // { id, key, data, createdAt, updatedAt } | null

// Interroger : where + orderBy + curseur, ≤ 100 lignes par page.
const page = await ctx.data.scores.query({
  where: { partyId: 'p_123', value: { gte: 10 } },
  orderBy: { field: 'createdAt', dir: 'desc' },
  limit: 20,
});
const next = page.nextCursor ? await ctx.data.scores.query({ where: { partyId: 'p_123' }, cursor: page.nextCursor }) : null;

// Compter, supprimer.
const { count } = await ctx.data.scores.count({ partyId: 'p_123' });
await ctx.data.favorites.delete('lyon');
```

Opérateurs de `where` par type (au plus **5 conditions**, `in` ≤ 20 valeurs) :

| Type | Opérateurs |
|---|---|
| `string` | `eq`, `ne`, `in`, `contains` |
| `number` | `eq`, `ne`, `in`, `gt`, `gte`, `lt`, `lte` |
| `date` | `eq`, `ne`, `gt`, `gte`, `lt`, `lte` |
| `boolean` | `eq`, `ne` |
| `json` | aucun (stockage seulement) |

`{ champ: valeur }` vaut `{ champ: { eq: valeur } }`. Le tri porte sur
`updatedAt` (défaut, décroissant), `createdAt` ou `key` — le tri par champ
de données viendra plus tard ; en attendant, triez la page côté code ou
utilisez la clé.

**Quota de lignes par app installée** (toutes collections) : 5 000 (Gratuit),
100 000 (Pro), 1 000 000 (Business). Au-delà, `set` d'une NOUVELLE clé
échoue avec un message clair — remplacer une ligne existante passe toujours.

## Fichiers privés — `ctx.files`

```json
{ "capabilities": ["files.private"] }
```

```ts
const f = await ctx.files.put('rapport.pdf', pdfBytes);     // Uint8Array, ArrayBuffer ou base64
// f = { id, name, contentType, size, url, createdAt }
return ui.link({ label: 'Télécharger le rapport', href: f.url });   // ou ui.image({ src: f.url, alt: '…' })

const { dataBase64 } = await ctx.files.get(f.id);
const { files } = await ctx.files.list();
await ctx.files.delete(f.id);
```

- Types acceptés : `png`, `jpg`, `webp`, `gif` (vérifiés par leurs octets de
  signature), `pdf`, `csv`, `txt`, `md`, `json` — ≤ **5 Mo** par fichier,
  500 fichiers par installation ; l'espace compte dans le **quota de
  stockage de la formule** de l'organisation (comme sa médiathèque).
- `url` est un chemin **relatif** vers une route gardée : seule une personne
  connectée à l'organisation ET autorisée à « utiliser l'app » peut l'ouvrir.
  Jamais d'URL publique, jamais de lien devinable.

## Réglages — `settings`

```json
{
  "settings": {
    "fields": [
      { "key": "defaultCity", "label": "Ville par défaut", "type": "text", "placeholder": "Paris" },
      { "key": "mode", "label": "Mode", "type": "select", "options": [{ "value": "simple", "label": "Simple" }, { "value": "expert", "label": "Expert" }] },
      { "key": "apiKey", "label": "Clé API du service tiers", "type": "secret", "required": true, "help": "Trouvez-la dans votre compte du service." }
    ]
  }
}
```

- Types : `text`, `textarea`, `number`, `boolean`, `select` (avec
  `options`), `secret`. Au plus 20 champs.
- Un **administrateur** remplit ces réglages sur la fiche de l'app
  (« Réglages de l'app ») ; votre code les lit dans `ctx.install.config`
  (`ctx.install.config.defaultCity`).
- Un champ `secret` est **chiffré au repos** (contexte imposé par la
  plateforme) et **jamais renvoyé à l'écran** : l'admin voit « défini » et
  peut le remplacer ; seul votre code hébergé reçoit la valeur en clair.
  Ne le journalisez pas (`ctx.log` est visible de l'admin).
- Les champs `config` des widgets continuent d'arriver dans le même
  `ctx.install.config` ; en cas de clé identique, le réglage déclaré prime.

## Cycle de vie, export, test

- **Désinstallation** : tout est conservé 30 jours (l'organisation voit
  « Données conservées » sur la fiche, avec « Effacer maintenant ») ; passé ce
  délai, la plateforme purge lignes, fichiers, KV et réglages.
- **Export** : l'admin exporte les données de votre app en JSON depuis la
  fiche ; elles font aussi partie de l'export RGPD de l'organisation
  (collections + métadonnées des fichiers + KV).
- **Pendant le développement** : la section **Données** de votre app
  (portail) montre les collections, le KV et les fichiers de vos
  installations de **test**, avec suppression ligne à ligne — jamais ceux
  d'une installation réelle. Retirer une install de test efface ses données.
