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 champsconfig
optionnels ({ key, label, type?, placeholder?, required? }) sont saisis
par un admin du tenant et arrivent dansctx.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é
- KV + fetch allowlisté), à télécharger depuis l’index de la
documentation — lisez-le, copiez-le, adaptez-le.