Référence
Capabilities, bornes et erreurs
Mis à jour le 4 septembre 2026Plateforme v1 Markdown brut
Capability ou permission ?
Deux champs du manifeste se ressemblent mais répondent à des questions
différentes :
capabilitiesré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…permissionsré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.
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). |
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). |
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). |
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) — 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). |
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). |
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é> :
{
"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 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) |
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 |
| 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) |
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) |
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) |
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 | 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 :
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. 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
Dockeraddons), 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) 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) 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.
C — Mode HYBRIDE (sans main ni backend). Votre propre backend
appelle l’API publique, vos pages sont montées côté
tenant via contributes.menuEntries (iframe isolée + jeton de contexte
signé), et les webhooks sortants 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éclarezui.widget, la review
s’attend à trouver une entrée correspondante danscontributes.widgets(et
réciproquement). http.fetchun 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.