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

> Chaque champ du manifeste add-on.json : type, contrainte exacte, règles de tarification croisées, erreurs de validation typiques et 3 manifestes complets.
> Catégorie : Référence · mis à jour le 2026-09-04 · plateforme v1

## 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](/dev/docs/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](/dev/docs/runtime)). 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](/dev/docs/moteurs)). Liste blanche stricte. |
| `events` | string[] | non | événements de domaine livrés à `defineApp({ events })` (catalogue fermé — cf. [Webhooks](/dev/docs/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" | "number" | "boolean" | "date" | "json" }, indexes? } }` — collections de données de l'app (`ctx.data.<nom>`), stockées chez l'organisation. Max 10 collections × 30 champs. Requiert la capability `data.collections` + `main`. Cf. [Données](/dev/docs/donnees). |
| `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](/dev/docs/donnees). |
| `author` | objet | oui | cf. [author](#author). |
| `compatibility` | objet | oui | cf. [compatibility](#compatibility). |
| `permissions` | string[] | non (défaut `[]`) | format `resource:action` — cf. [Permissions et consentement du tenant](/dev/docs/permissions). |
| `capabilities` | string[] | non (défaut `[]`) | sous-ensemble de la liste blanche — cf. [Capabilities, bornes et erreurs](/dev/docs/capabilities). |
| `contributes` | objet | non | points d'extension déclaratifs — cf. [contributes](#contributes). |
| `pricing` | objet | oui | cf. [pricing](#pricing). |
| `i18n` | objet | oui | cf. [i18n](#i18n). |
| `support` | objet | non | cf. [support](#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](/dev/docs/permissions).

---

### `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](/dev/docs/capabilities).
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](/dev/docs/runtime) et [Le Kit](/dev/docs/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](/dev/docs/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](/dev/docs/widgets-declaratifs) ;
- **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](/dev/docs/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 :

```json
{
  "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](/dev/docs/donnees)) — 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](/dev/docs/revenus).

---

### `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](/dev/docs/permissions) : `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](/dev/docs/permissions). |
| `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é

```json
{
  "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

```json
{
  "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é

```json
{
  "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](/dev/docs/facades) de l'[API publique](/dev/docs/api-publique)
(clé API + `X-Capibara-Install`) et reçoit les [webhooks
sortants](/dev/docs/webhooks) ; 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](/dev/docs/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](/dev/docs) — lisez-le, copiez-le, adaptez-le.
