Documentation

Référence

Référence du manifeste add-on.json

Mis à jour le 4 septembre 2026Plateforme v1 Markdown brut

Le fichier add-on.json

add-on.json est l’unique fichier de configuration d’une app Capibara. Il
est validé par un schéma strict : tout champ de premier niveau qui n’est
pas listé ci-dessous est refusé (pas ignoré silencieusement — la
validation échoue avec la liste des clés inconnues). Ce document reflète
exactement le validateur serveur ; le portail utilise le même schéma pour la
complétion et la validation en direct dans l’éditeur de code.

Champs de premier niveau

Champ Type Obligatoire Contrainte
key string oui ^[a-z0-9](?:[a-z0-9-]{1,48}[a-z0-9])?$ — kebab-case, 3 à 50 caractères en pratique (imposé à la création du projet). Immuable : doit rester identique à la key du projet sur toute nouvelle version.
name string oui 2 à 80 caractères.
version string oui semver strict MAJOR.MINOR.PATCH, avec suffixe de pré-version optionnel (1.2.0, 2.0.0-beta.1).
main string non point d’entrée du code hébergé (chemin relatif du dépôt, .ts/.tsx/.js/.mjs, jamais de .. — ex. src/index.ts). Le module exporte export default defineApp({ … }) (cf. Le runtime) ; il est bundlé automatiquement à la publication depuis votre dépôt Git (imports relatifs + @capibara-dev/sdk, pas de node_modules). Requis (ou backend) dès qu’une entrée déclare view, qu’un widget est kind: "kit", ou que panels, siteBlocks, sitePages, events, schedules ou http sont déclarés ; requis SEUL pour collections, files.private et les moteurs.
backend objet non { url } — backend externe (DV-9) : votre serveur reçoit les requêtes du protocole (écrans, actions, événements, planifications, HTTP) en POST signé et horodaté et répond du Kit (cf. Le runtime › Backend externe). HTTPS, hôte public (jamais une IP, ni localhost, ni domaine interne). Exclusif de main. Consentement explicite de l’organisation, badge « Backend chez l’éditeur », review humaine obligatoire.
returnHosts string[] non domaines (minuscules, ≤ 5) vers lesquels vos liens de paiement peuvent renvoyer le client (returnUrl de billing.documents.payLink / billing.charges.create, cf. Moteurs › Paiements). Liste blanche stricte.
events string[] non événements de domaine livrés à defineApp({ events }) (catalogue fermé — cf. Webhooks) ; requiert la capability events.subscribe. Max 20.
schedules objet[] non [{ key, cron }] — planifications exécutées par la plateforme (cron à 5 champs, heure de Paris, intervalle minimum selon le plan du tenant). Max 5.
http string[] non routes HTTP entrantes (["/stripe-hook"]) servies par defineApp({ http }) sur une URL publique signée par installation. Max 5.
collections objet non `{ <nom>: { fields: { <champ>: “string”
settings objet non { fields: [{ key, label, type, options?, required?, placeholder?, help? }] } — réglages saisis par un administrateur de l’organisation, lus dans ctx.install.config ; type secret = chiffré au repos, jamais réaffiché. Max 20 champs. Cf. Données.
author objet oui cf. author.
compatibility objet oui cf. compatibility.
permissions string[] non (défaut []) format resource:action — cf. Permissions et consentement du tenant.
capabilities string[] non (défaut []) sous-ensemble de la liste blanche — cf. Capabilities, bornes et erreurs.
contributes objet non points d’extension déclaratifs — cf. contributes.
pricing objet oui cf. pricing.
i18n objet oui cf. i18n.
support objet non cf. support.

author

Champ Type Obligatoire Contrainte
author.name string oui 1 à 80 caractères. Nom affiché.
author.devAccountSlug string oui 1 à 40 caractères. Doit être exactement le slug de votre compte développeur (affiché sur votre tableau de bord, champ « Identifiant développeur ») — sinon le serveur refuse l’enregistrement avec le message author.devAccountSlug doit être "<votre-slug>".

compatibility

Champ Type Obligatoire Contrainte
compatibility.colibri string oui non vide — plage de version de plateforme requise (ex. ">=1.0").
compatibility.modules string[] non (défaut []) clés des modules qui doivent être actifs chez le tenant pour que l’app ait un sens (ex. ["crm"], ["billing-fr"]). N’active rien tout seul : c’est une condition affichée/vérifiée, pas une activation automatique.

Note de nommage. Le champ s’appelle compatibility.colibri (et non
capibara) : c’est un vestige du nom du moteur technique interne de la
plateforme. Il désigne bien la version de Capibara — écrivez-le tel
quel dans votre manifeste, la clé ne change pas.


permissions

Tableau de chaînes au format resource:action, validées par
^[a-z][a-z0-9-]*:(read|write|delete|admin)$. Documentation complète —
ressources disponibles, consentement du tenant, bonnes pratiques — dans
Permissions et consentement du tenant.


capabilities

Tableau de chaînes, chacune soit dans la liste blanche fixe — storage.tenant.ro,
storage.tenant.rw, ui.block, ui.menu, ui.widget, ui.panel,
ui.site, ui.settings, ai.tool, events.subscribe, data.collections,
files.private, mail.send, notify.send, calendar.project,
search.index, approvals.request —, soit de la forme http.fetch:<domaine>
avec un domaine pleinement qualifié (^http\.fetch:[a-z0-9.-]+\.[a-z]{2,}$, ex.
http.fetch:api.example.com). Détail de chaque capability, des bornes
d’exécution et des erreurs dans Capabilities, bornes et erreurs.
Certaines exigent main (code hébergé) : data.collections,
files.private et les cinq moteurs (mail.send, notify.send,
calendar.project, search.index, approvals.request).


contributes

Points d’extension, tous optionnels. Deux familles :

Typées et MONTÉES dès l’installation :

contributes.menuEntries (max 5, capability ui.menu requise) — chaque
entrée apparaît dans la navigation du tenant et ouvre votre page. Deux formes,
exactement une des deux par entrée :

  • view (mode hébergé, recommandé) : la clé d’une page du Kit rendue par
    votre code hébergé — defineApp({ pages: { <view>: { render } } }).
    Capibara affiche le JSON renvoyé avec ses propres composants : même look que
    le reste de l’application, aucun serveur à héberger, aucun HTML ni
    JavaScript à écrire. Cf. Le runtime et Le Kit.
  • embedUrl (mode hybride) : votre page HTTPS, hébergée chez vous,
    embarquée dans une iframe isolée avec un jeton de contexte signé
    ?capibara_token=<jwt> que votre backend résout via
    GET /api/public/v1/embed-context (cf. API publique).
Champ Type Règle
key string kebab-case, unique dans l’app, IMMUABLE (devient le segment d’URL /apps/<app>/<key>).
label string 2-40 caractères.
labelI18n objet optionnel — { fr?, en?, es?, de? } (≤ 40 caractères chacun).
view string mode hébergé — clé kebab-case d’une page de defineApp({ pages }) ; requiert main.
embedUrl string mode hybride — HTTPS obligatoire, domaine public (jamais d’IP ni de localhost), ≤ 500 caractères.
icon string optionnel — nom d’icône Lucide kebab-case (défaut : puzzle).

contributes.widgets (max 5, capability ui.widget requise) — un widget du
tableau de bord d’accueil, trois familles :

  • Kit (recommandé) : { key, title (2-60), kind: "kit", config?, minHeight? } — rendu par votre code hébergé
    (defineApp({ widgets: { <key>: { render } } })) ; les champs config
    optionnels ({ key, label, type?, placeholder?, required? }) sont saisis
    par un admin du tenant et arrivent dans ctx.install.config ;
  • déclaratif : { key, title, kind: "declarative", template, config?, source?, minHeight? } — zéro code : la plateforme collecte la donnée
    et rend le widget. Référence : Widgets déclaratifs ;
  • embed : { key, title, embedUrl, minHeight? } — votre page hébergée
    chez vous, même iframe isolée + jeton que les entrées de menu hybrides.

Les utilisateurs du tenant ajoutent les widgets depuis « Personnaliser » sur
leur accueil (rubrique Apps du sélecteur).

contributes.panels (max 5, capability ui.panel requise, main requis)
— { key, label (2-60), subject: "party" } : un panneau rendu par votre code
(defineApp({ panels: { <key>: { render } } })) sur les fiches contact et
société
du CRM du tenant, avec ctx.subject = { type: 'party', id }. Il
ne s’affiche qu’aux personnes qui peuvent lire la fiche. party est le seul
sujet ouvert aujourd’hui (les fiches des autres modules viendront avec leur
propre valeur de subject).

contributes.siteBlocks (max 10, capability ui.block requise, main
requis) — { key, label (2-60), summary? (≤ 140), props? } : un bloc du
site public
de l’organisation, rendu par votre code hébergé
(defineApp({ siteBlocks: { <key>: { render } } }), surface siteBlock).
L’auteur du site le pose depuis l’éditeur de page (« Ajouter » › groupe « Vos
apps ») ; les champs props ({ key, label, type?: "text" | "number", placeholder?, required? }, ≤ 6) se remplissent dans l’inspecteur du bloc et
arrivent dans params de votre handler. Sur le site public, viewer est
toujours null : le visiteur (membre du site connecté ou anonyme) arrive
dans visitor — cf. Le runtime.

contributes.sitePages (max 5, capability ui.site requise, main
requis) — { key, title (2-80), slug?, description? (≤ 160), noindex? } :
une page entière du site public, rendue par
defineApp({ sitePages: { <key>: { render } } }) (surface sitePage,
≤ 500 composants). slug n’est qu’une adresse PROPOSÉE : l’organisation
active la page et choisit son adresse dans Mon site › Apps (jamais un slug
réservé aux pages de Capibara, jamais l’adresse d’une page existante du site —
la page du site garde toujours la priorité). Les sous-chemins
(/<slug>/<…>) arrivent dans params.path, les paramètres d’URL dans
params.query ; description et noindex alimentent le référencement et
le sitemap.

Exemple complet :

{
  "capabilities": ["ui.menu"],
  "contributes": {
    "menuEntries": [
      {
        "key": "tableau",
        "label": "Tableau de bord",
        "labelI18n": { "en": "Dashboard" },
        "embedUrl": "https://app.votre-domaine.com/capibara",
        "icon": "gauge"
      }
    ]
  }
}

Votre page reçoit ?capibara_token=… et s’affiche dans une iframe
sandbox sans allow-same-origin : elle ne peut ni lire les cookies
Capibara ni toucher le DOM parent — concevez-la comme une page autonome.

Encore libres (typage à l’ouverture de leur montage) — acceptées comme
tableaux libres (≤ 10 entrées chacun), enregistrées et lisibles en review,
mais pas encore montées côté tenant : ne comptez pas dessus pour un
comportement visible.

Clé Nécessite la capability En attendant
settingsSections ui.settings les réglages passent par settings.fields (cf. Données) — c’est ce qu’un administrateur remplit réellement.
aiTools ai.tool aucun équivalent : l’assistant IA de la plateforme n’appelle pas encore les apps.

pricing

Champ Type Règle
pricing.model 'free' | 'one_time' | 'subscription' obligatoire.
pricing.amountEur number requis et > 0 dès que model ≠ free ; interdit (doit être absent ou 0) quand model = free.
pricing.trialDays integer 0–30 autorisé uniquement quand model = subscription. Sur one_time, le renseigner est une erreur.

Trois combinaisons valides, aucune autre :

model amountEur trialDays
free absent interdit
one_time > 0 requis interdit
subscription > 0 requis optionnel, 0–30

Le prix est saisi en euros ; au lancement, la conversion multi-devise au
paiement est gérée par Stripe selon le pays du payeur. Le partage de revenu
qui s’applique à ce prix est détaillé dans Revenus et
rev-share
.


i18n

Champ Type Obligatoire Contrainte
i18n.fr string oui 1 à 120 caractères — la baseline utilisée comme accroche par défaut de la fiche marketplace.
i18n.en string oui 1 à 120 caractères.
i18n.es string non ≤ 120 caractères.
i18n.de string non ≤ 120 caractères.

fr et en sont la baseline obligatoire — un manifeste sans l’une des
deux est invalide, même si le reste est correct.


support

Champ Type Contrainte
support.email string format e-mail valide.
support.docsUrl string URL valide (http(s)://…).

Non obligatoire au sens du schéma, mais fortement attendu en review : un
app sans e-mail de support est plus difficile à faire approuver, et bloque
la fonctionnalité « Contacter le développeur » que voit un tenant qui a
installé votre app.


Erreurs de validation courantes

Message renvoyé Cause Correction
Identifiant invalide (a-z, 0-9, tirets ; 3 à 50 caractères). key ne respecte pas le format kebab-case, ou commence/finit par un tiret. Utilisez uniquement minuscules, chiffres et tirets internes (ex. crm-segments).
Version semver invalide (ex. 1.2.0). version n’est pas au format MAJOR.MINOR.PATCH. 1.0.0, pas v1.0 ni 1.0.
Permission invalide (format resource:action). Une entrée de permissions ne suit pas resource:action. crm.contacts:read, pas crm.contacts.read ni read-crm.
Scope trop large : « crm:read » donnerait tout le module… Une entrée de permissions vise un module entier au lieu d’une ressource feuille. Prenez un scope du catalogue : crm.contacts:read, crm.opportunities:read…
Scope inconnu : « … » Le scope n’est pas dans le catalogue (rien ne l’ouvrirait). Consultez la liste sur Scopes et consentement.
Capability inconnue (ou domaine http.fetch manquant/mal formé). Une entrée de capabilities n’est ni dans la liste blanche, ni un http.fetch:<domaine> valide. Vérifiez l’orthographe exacte (ui.widget, pas ui.widgets) ou complétez le domaine (http.fetch:api.example.com, pas http.fetch: seul).
Montant > 0 requis pour une app payante. pricing.model ≠ free mais amountEur absent ou ≤ 0. Renseignez amountEur.
Une app gratuite ne doit pas avoir de montant. pricing.model = free avec un amountEur > 0. Retirez amountEur, ou changez model.
Les essais ne s'appliquent qu'aux abonnements. trialDays renseigné avec model = one_time. Retirez trialDays, ou passez en subscription.
Baseline FR obligatoire / Baseline EN obligatoire i18n.fr ou i18n.en vide. Renseignez une phrase courte dans les deux langues.
Erreur de type « clé non reconnue » sur le manifeste Un champ de premier niveau ne fait pas partie de la liste (le schéma est strict). Retirez le champ, ou vérifiez l’orthographe (compatibility, pas compatibilities).
Le champ "key" doit rester "<clé du projet>" (immuable). Le key du manifeste ne correspond plus à celui du projet créé sur le portail. Remettez la key d’origine — pour changer d’identifiant, créez un nouveau projet.
author.devAccountSlug doit être "<votre-slug>". author.devAccountSlug ne correspond pas à votre compte développeur. Copiez exactement votre slug depuis le tableau de bord du portail.
La version X est déjà REVIEW/APPROVED/PUBLISHED et ne peut être modifiée. Incrémentez la version. Vous tentez de réenregistrer un brouillon sur un version déjà engagé dans le cycle. Changez version (ex. 0.1.0 → 0.1.1) avant d’enregistrer.

Trois exemples de manifestes complets

Les trois passent le validateur tel quel (à author.devAccountSlug près,
qui doit être le vôtre).

1. Gratuit, avec un widget rendu par du code hébergé

{
  "key": "crm-segments",
  "name": "Segments avancés CRM",
  "version": "0.1.0",
  "main": "src/index.ts",
  "author": { "name": "Atelier TS", "devAccountSlug": "atelier-ts" },
  "compatibility": { "colibri": ">=1.0", "modules": ["crm"] },
  "permissions": ["crm.contacts:read"],
  "capabilities": ["ui.widget", "storage.tenant.rw"],
  "contributes": {
    "widgets": [
      { "key": "segments", "title": "Mes segments", "kind": "kit", "minHeight": 200 }
    ]
  },
  "pricing": { "model": "free" },
  "i18n": {
    "fr": "Segmentez vos contacts CRM en un clic.",
    "en": "Segment your CRM contacts in one click."
  },
  "support": { "email": "support@atelier-ts.example" }
}

2. Payant, abonnement avec essai — événements, planification, e-mail, réglages

{
  "key": "devis-relance-auto",
  "name": "Relances automatiques de devis",
  "version": "1.2.0",
  "main": "src/index.ts",
  "author": { "name": "Studio Forge", "devAccountSlug": "studio-forge" },
  "compatibility": { "colibri": ">=1.0", "modules": ["billing-fr"] },
  "permissions": ["billing-fr.documents:read", "crm.contacts:read"],
  "capabilities": ["events.subscribe", "mail.send", "storage.tenant.rw"],
  "events": ["quote.created", "quote.accepted"],
  "schedules": [{ "key": "relances", "cron": "0 9 * * *" }],
  "settings": {
    "fields": [
      { "key": "delays", "label": "Délais de relance (jours)", "type": "text", "placeholder": "3, 7, 14" }
    ]
  },
  "pricing": { "model": "subscription", "amountEur": 9, "trialDays": 14 },
  "i18n": {
    "fr": "Relance automatiquement vos devis non signés après 3, 7 et 14 jours.",
    "en": "Automatically follows up on unsigned quotes after 3, 7 and 14 days."
  },
  "support": {
    "email": "support@studio-forge.example",
    "docsUrl": "https://studio-forge.example/docs/devis-relance"
  }
}

3. Hybride : page embarquée + backend chez vous, sans code hébergé

{
  "key": "sync-compta-externe",
  "name": "Synchronisation comptable externe",
  "version": "0.3.1",
  "author": { "name": "Studio Forge", "devAccountSlug": "studio-forge" },
  "compatibility": { "colibri": ">=1.0", "modules": ["billing-fr"] },
  "permissions": ["billing-fr.documents:read"],
  "capabilities": ["ui.menu"],
  "contributes": {
    "menuEntries": [
      { "key": "synchro", "label": "Synchronisation", "embedUrl": "https://app.studio-forge.example/capibara", "icon": "refresh-cw" }
    ]
  },
  "pricing": { "model": "one_time", "amountEur": 49 },
  "i18n": {
    "fr": "Exporte vos factures vers votre logiciel comptable, depuis un backend hébergé chez l'éditeur.",
    "en": "Exports your invoices to your accounting software, from a backend hosted by the publisher."
  },
  "support": { "email": "support@studio-forge.example" }
}

Cette app illustre le modèle hybride : ni main ni backend — son
vrai travail tourne dans votre backend, qui lit les factures par les
façades de l’API publique
(clé API + X-Capibara-Install) et reçoit les webhooks
sortants
; sa page est montée en iframe isolée avec un
jeton de contexte. Aucun http.fetch:<domaine> n’est nécessaire : cette
capability n’a de sens que pour le code hébergé (main), où elle est
l’allowlist réseau réellement appliquée par la sandbox — tout appel vers un
host non déclaré y est refusé à l’exécution. Pour servir les écrans depuis
votre serveur avec le protocole du Kit, voyez plutôt
Backend externe (backend: { url }).

Exemple complet téléchargeable. L’app « Météo (exemple) » réunit tout
ce qui précède dans un projet fonctionnel (page et widget du Kit rendus par le code hébergé

Référence du manifeste add-on.json · Capibara for Developers