# Capabilities, bornes et erreurs

> Chaque capability du manifeste, ce qu'elle autorise, quand la demander — et, en un seul endroit, les bornes réelles de l'exécution (temps, tailles, appels, concurrence, disjoncteur) et les erreurs que votre code peut rencontrer.
> Catégorie : Référence · mis à jour le 2026-09-04 · plateforme v1

## Capability ou permission ?

Deux champs du manifeste se ressemblent mais répondent à des questions
différentes :

- **`capabilities`** répond à *« à quel type d'extension ou de ressource
  technique votre app veut-elle accéder ? »* — une entrée de menu, un appel
  réseau sortant, un espace de stockage clé/valeur…
- **`permissions`** répond à *« à quelles données métier du tenant votre
  app veut-elle accéder ? »* — lire les contacts CRM, écrire des factures…
  Documentation complète : [Permissions et consentement du
  tenant](/dev/docs/permissions).

Une app qui affiche un widget lisant des contacts CRM déclare donc
généralement **les deux** : `"capabilities": ["ui.widget"]` et
`"permissions": ["crm.contacts:read"]`.

---

## Liste blanche des capabilities

Seules les capabilities explicitement listées ci-dessous (ou un
`http.fetch:<domaine>` bien formé) sont acceptées par le validateur du
manifeste — toute autre valeur est rejetée à l'enregistrement.

| Capability | Autorise | Quand la demander |
|---|---|---|
| `storage.tenant.ro` | Lecture d'un espace de stockage clé/valeur scopé au tenant qui a installé l'app. | Votre app a besoin de relire une configuration qu'il a écrite précédemment, sans avoir à la redemander à l'utilisateur. |
| `storage.tenant.rw` | Lecture **et** écriture de ce même espace de stockage. | Votre app doit mémoriser un réglage, un état, un cache léger propre à ce tenant. |
| `ui.block` | Contribuer un **bloc au site public** de l'organisation via `contributes.siteBlocks` — posé par l'auteur du site dans l'éditeur de page (groupe « Vos apps »), rendu par votre code hébergé (cf. [Le runtime](/dev/docs/runtime)). | Votre app ajoute une section (météo, catalogue, formulaire…) que l'utilisateur pose sur une page publique de son site. |
| `ui.menu` | Contribuer une entrée dans la navigation latérale — page du Kit rendue par votre code hébergé (`view`) ou page HTTPS montée en iframe isolée (`embedUrl`) via `contributes.menuEntries` (cf. [le manifeste](/dev/docs/manifeste)). | Votre app a sa propre page dans l'espace de gestion du tenant. |
| `ui.widget` | Contribuer un widget sur le tableau de bord d'accueil via `contributes.widgets` — Kit (code hébergé), déclaratif ou embed ; ajout depuis « Personnaliser ». | Votre app affiche un résumé ou un indicateur en un coup d'œil. |
| `ui.panel` | Contribuer un panneau sur une **fiche** du tenant (contact/société du CRM) — rendu par votre code hébergé via `contributes.panels` (cf. [Le runtime](/dev/docs/runtime)). | Votre app montre, sur la fiche d'un client, un score, un historique, des actions. |
| `ui.site` | Contribuer une **page entière au site public** via `contributes.sitePages` — activée et adressée par l'organisation dans Mon site › Apps, rendue par votre code hébergé. | Votre app a besoin d'une adresse à lui sur le site de l'organisation (catalogue, prise de rendez-vous, espace dédié…). |
| `ui.settings` | Réservée : contribuer une section dans la centrale de paramétrage du tenant (`contributes.settingsSections`, **pas encore montée**). Les réglages d'une app passent aujourd'hui par `settings.fields` du manifeste (cf. [Données](/dev/docs/donnees)) — aucune capability nécessaire. | Ne la demandez pas encore : elle n'ouvre rien de visible. |
| `ai.tool` | Réservée : exposer un outil à l'assistant IA de la plateforme (`contributes.aiTools`, **pas encore monté**). | Ne la demandez pas encore : l'assistant n'appelle pas les apps aujourd'hui. |
| `events.subscribe` | Souscrire aux événements de domaine typés émis par les apps (ex. facture payée, devis accepté) — livrés à votre code hébergé (`events` du manifeste + `defineApp({ events })`) ou à un webhook sortant. | Votre app réagit à un événement métier plutôt que d'interroger l'API en boucle. |
| `data.collections` | Lire et écrire les **collections** déclarées dans `collections` du manifeste (`ctx.data.<nom>`) — enregistrements typés, validés, stockés chez l'organisation (cf. [Données](/dev/docs/donnees)). | Votre app a de vraies données structurées (favoris, scores, tickets…) et ne veut ni base à gérer ni migration. |
| `files.private` | Déposer et relire des **fichiers privés** (`ctx.files`, ≤ 5 Mo, types bornés), servis par une route gardée de l'organisation. | Votre app génère un PDF, une image ou un export à remettre aux personnes autorisées. |
| `mail.send` | Envoyer un **e-mail transactionnel** (`ctx.mail.send`) à des personnes liées à l'organisation — ≤ 5 par appel, contenu en blocs, coque de l'organisation, désinscription en un clic, quota par jour (cf. [Moteurs](/dev/docs/moteurs)). | Votre app confirme, rappelle ou relance une personne précise. Jamais une campagne. |
| `notify.send` | Notifier dans Capibara (`ctx.notify.user / permission / admins`) — cloche + push, quota par jour. | Votre app a quelque chose à dire à une personne, aux porteurs d'une permission ou aux administrateurs. |
| `calendar.project` | Projeter des dates dans le calendrier de l'organisation (`ctx.calendar`, source `app:<clé>`, ≤ 500). | Vos objets ont des dates (échéances, interventions, sessions) qui méritent d'apparaître dans le planning. |
| `search.index` | Rendre vos fiches trouvables dans la recherche ⌘K (`ctx.search`, ≤ 2 000), groupe au nom de l'app, visible des personnes qui ont « utiliser l'app ». | Vos utilisateurs cherchent vos objets par leur nom depuis n'importe quel écran. |
| `approvals.request` | Demander une validation à un humain (`ctx.approvals.request`) — cloche « Validations », décision livrée par l'événement `approval.decided`. | Une action de votre app engage l'organisation (remboursement, commande) et doit être tranchée par quelqu'un. |

### Accès réseau — `http.fetch:<domaine>`

Pas de capability fixe pour le réseau sortant : chaque domaine externe que
votre app appelle doit être déclaré explicitement, un par un, sous la
forme `http.fetch:<domaine pleinement qualifié>` :

```json
{
  "capabilities": ["http.fetch:api.exemple.com", "http.fetch:hooks.exemple.io"]
}
```

Un domaine sans point ni TLD d'au moins deux lettres est refusé
(`http.fetch:localhost` ou `http.fetch:api` ne valident pas). Il n'y a pas
d'accès au réseau interne de la plateforme : seul l'egress vers des domaines
publics explicitement déclarés passe, et chaque appel de `ctx.fetch` est
proxifié par la plateforme (HTTPS seul, 10 s, réponse ≤ 512 Ko, aucune
redirection suivie) et audité — cf. les bornes ci-dessous. Cette capability
ne concerne que le **code hébergé** : une app **hybride** ou à **backend
externe** fait ses appels réseau depuis son propre serveur, et appelle
l'[API publique](/dev/docs/api-publique) de Capibara avec une clé API ou un
jeton OAuth, comme n'importe quel client HTTP.

---

## Bornes, quotas et erreurs (référence unique)

Tout ce qui borne l'exécution d'une app est réuni ici ; les autres pages y
renvoient. Ces valeurs sont **celles appliquées aujourd'hui** par la
plateforme — pas des intentions.

### Une interaction (page, action, événement, planification, HTTP entrant)

| Borne | Valeur | Dépassement |
|---|---|---|
| Durée d'une interaction | **15 s** (code hébergé et backend externe) | l'interaction échoue : « L'app n'a pas répondu à temps », ligne d'erreur au journal |
| Corps envoyé à votre code | ≤ 256 Ko | refus avant exécution |
| Réponse de votre code | ≤ 1 Mo | refus, l'écran dit « L'app n'a pas répondu » |
| Appels au broker par interaction (`ctx.kv`, `ctx.fetch`, `ctx.capibara`, moteurs…) | ≤ 100 | `quota_exceeded` : l'appel suivant lève, l'interaction échoue |
| Journal `ctx.log` | 200 lignes × 500 caractères par interaction | lignes suivantes ignorées |
| État `state` renvoyé | ≤ 8 Ko, signé par la plateforme, lié à l'installation et à la surface, 24 h | rejeté (état absent au prochain appel) |
| `followup` après un `ack` | dans les 15 min de l'`ack` | `followup_rejected` |
| Document du Kit | `page` / `sitePage` ≤ 500 composants, autres surfaces ≤ 200, profondeur 8, 32 Ko de texte (cf. [Le Kit](/dev/docs/kit)) | non affiché, raison au journal |

### Par app et par organisation

| Borne | Valeur | Dépassement |
|---|---|---|
| Interactions simultanées (par app, tous tenants) | ≤ 8 | `busy` : réponse 503 « occupée », réessayez |
| Disjoncteur | ≥ 10 échecs en 5 min **et** ≥ 50 % des appels → app suspendue **5 min**, puis une sonde | `open` : 503 pendant la suspension, visible au journal |
| Kill switch | incident posé par l'équipe Capibara (global ou pour une organisation) | `killed` : toute invocation refusée jusqu'à la levée — cf. [Publication et review](/dev/docs/publication) |
| Planifications | intervalle minimum Gratuit 1 h · Pro 15 min · Business 5 min ; une seule exécution en vol par planification | occurrences sautées |
| HTTP entrant | corps ≤ 256 Ko, JSON ou texte, rate-limit par IP | 413 / 429 au tiers appelant |

### `ctx.*` — bornes par capability

| Capability | Bornes |
|---|---|
| `storage.tenant.*` (`ctx.kv`) | clé 1–128 caractères (`a-z 0-9 _ . : -`), **valeur ≤ 32 Ko**, **200 clés** par installation, `list` paginé |
| `http.fetch:<host>` (`ctx.fetch`) | HTTPS seul, host déclaré seul, **10 s**, réponse **≤ 512 Ko** (`truncated: true` au-delà), **aucune redirection** suivie (`redirect: 'error'`) |
| `data.collections` (`ctx.data`) | 10 collections × 30 champs, enregistrement ≤ 64 Ko, 100 lignes par page, 5 conditions, lignes par installation : 5 000 / 100 000 / 1 000 000 selon la formule (cf. [Données](/dev/docs/donnees)) |
| `files.private` (`ctx.files`) | ≤ 5 Mo par fichier, 500 fichiers par installation, types bornés, quota de stockage de la formule |
| `mail.send` | ≤ 5 destinataires liés par appel, quota par jour et par installation 20 / 200 / 1 000 (cf. [Moteurs](/dev/docs/moteurs)) |
| `notify.send` | quota par jour 200 / 2 000 / 10 000 ; `permission` ≤ 50 personnes |
| `calendar.project` | ≤ 500 projections, ≤ 366 jours |
| `search.index` | ≤ 2 000 fiches |
| `approvals.request` | ≤ 200 demandes en attente |
| Façades `ctx.capibara.*` | `take` ≤ 100 ; en REST, 120 appels / minute par installation (cf. [Façades](/dev/docs/facades)) |

### Les erreurs que votre code voit

Un appel `ctx.*` refusé **lève une exception** dans votre handler (le
message dit pourquoi) ; l'interaction échoue proprement si vous ne
l'attrapez pas, et le journal de l'app porte le code :

| Code | Sens | Ce qu'il faut faire |
|---|---|---|
| `not_granted` | capability absente du manifeste, ou non consentie par l'organisation (scope) | déclarez-la, puis faites re-consentir (nouvelle version) |
| `forbidden` | borne dépassée : clé KV invalide, valeur > 32 Ko, quota de clés, host non déclaré, type de fichier refusé… | corrigez l'appel ; le message nomme la borne |
| `quota_exceeded` | plus de 100 appels broker dans l'interaction | regroupez vos lectures, mettez en cache dans `state` ou `kv` |
| `killed` | incident actif sur la version (kill switch) | attendez la levée ; l'e-mail d'incident vous a dit le motif |
| `scope_not_granted`, `invalid_input`, `not_found`, `conflict`, `precondition_failed`, `service_account_missing` | réponses des [façades](/dev/docs/facades) | cf. leur table |
| `handler_missing` | la capability existe mais aucune implémentation ne la sert sur cette instance | contactez le support de l'instance |

Côté organisation, une app qui échoue trop (disjoncteur) ou qui fait l'objet
d'un incident est isolée **sans affecter les autres apps ni les autres
organisations** ; chaque appel qui passe par le broker est audité (journal
consultable par l'équipe de review). Ni le temps CPU ni la mémoire ne sont
mesurés individuellement aujourd'hui : c'est le délai de 15 s et
l'isolation de la sandbox qui bornent une interaction.

### Trois façons de faire tourner votre logique

**A — Code HÉBERGÉ (sandbox).** Ajoutez `main` à votre manifeste (chemin
relatif, ex. `"main": "src/index.ts"`). À la publication, Capibara empaquette
ce point d'entrée (sans jamais exécuter votre code au build) et le fait tourner
dans un isolate contraint (workerd). Votre module exporte un handler façon
worker :

```ts
import { defineApp, ui } from '@capibara-dev/sdk';

export default defineApp({
  pages: {
    home: async ({ ctx }) => {
      // ctx = votre seule porte vers la plateforme (servie par le broker, qui
      // applique capabilities + permissions + quotas + audit).
      await ctx.kv.put('compteur', { n: 1 });                       // storage.tenant.rw
      const row = await ctx.kv.get('compteur');                      // storage.tenant.ro
      const r = await ctx.fetch('https://api.exemple.com/x');        // http.fetch:api.exemple.com
      return ui.text({ markdown: `Compteur ${row?.value?.n} — amont ${r.status}` });
    },
  },
});
```

Votre code est invoqué par la plateforme à chaque interaction (page ouverte,
clic, formulaire, événement, planification, requête HTTP entrante) — cf.
[Le runtime](/dev/docs/runtime). `ctx.kv` expose `get/list/put/delete` (KV
cloisonné par organisation) et `ctx.fetch(url, init)` la sortie réseau bornée,
vers les domaines `http.fetch:` déclarés. Aucun autre accès : ni DB, ni
réseau libre, ni secret. Les bornes ci-dessus s'appliquent à ce code.

> **Activation.** Le runtime hébergé est un service de l'instance (profil
> Docker `addons`), **actif par défaut** sur capibara.fr — chaque publication
> est rechargée automatiquement en quelques secondes. Sur une instance où il
> serait désactivé, l'invocation renvoie proprement « runtime non activé » —
> votre app reste distribuée et installable, et son mode hybride
> (ci-dessous) fonctionne sans dépendre du runtime.

**Écrans** : vos pages, widgets et panneaux sont décrits en JSON (le
[Kit](/dev/docs/kit)) et rendus par Capibara avec ses composants — jamais de
HTML ni de JavaScript côté client. L'exemple « Météo » (téléchargeable depuis
l'[index](/dev/docs)) montre le motif complet.

**B — BACKEND EXTERNE (`backend: { url }`, sans `main`).** Le même
`defineApp`, servi par VOTRE serveur, avec les mêmes bornes et le même
disjoncteur ; les façades passent par l'API publique, et collections,
fichiers et moteurs restent réservés au code hébergé. Tout est sur la page
dédiée [Backend externe](/dev/docs/backend-externe).

**C — Mode HYBRIDE (sans `main` ni `backend`).** Votre propre backend
appelle l'[API publique](/dev/docs/api-publique), vos pages sont montées côté
tenant via `contributes.menuEntries` (iframe isolée + jeton de contexte
signé), et les [webhooks sortants](/dev/docs/webhooks) vous poussent les
événements. Ce chemin ne dépend d'aucune activation et fonctionne dès
aujourd'hui. A et C se combinent librement ; B est exclusif de A.

---

## Déclarer des capabilities : bonnes pratiques

- **Minimum nécessaire** — chaque capability demandée est visible par la
  review ; l'administrateur du tenant voit, lui, les scopes (phrases de
  consentement) et l'hébergement (« Tourne chez Capibara » / « Backend chez
  l'éditeur ») au moment de l'installation. N'en demandez que ce que votre
  app utilise vraiment.
- **Cohérence avec `contributes`** — si vous déclarez `ui.widget`, la review
  s'attend à trouver une entrée correspondante dans `contributes.widgets` (et
  réciproquement).
- **`http.fetch` un domaine à la fois** — pas de joker, pas de sous-domaine
  générique : un domaine par service tiers que vous appelez réellement.
- **Pas de capability « au cas où »** — en ajouter une que vous n'utilisez
  pas encore ralentit la review et n'apporte rien tant qu'elle n'est pas
  exercée.
