Documentation

Référence

Le runtime — defineApp

Mis à jour le 4 septembre 2026Plateforme v1 Markdown brut

Le principe

Une app Capibara fonctionne comme un bot Discord : la plateforme lui
envoie des interactions (ouverture d’une page, clic, soumission d’un
formulaire, événement métier, planification, requête HTTP d’un tiers) ; votre
code répond par une description d’écran (le Kit) ou par
des effets (toast, navigation, rafraîchissement…). Vous n’hébergez rien :
le code déclaré par main est bundlé à la publication et tourne dans la
sandbox Capibara (workerd sous gVisor), sans Node.js, sans réseau libre, sans
secret. Sa seule porte vers le monde est le ctx reçu par chaque handler.

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

export default defineApp({
  pages: {
    home: {
      async render({ ctx, viewer }) {
        const row = await ctx.kv.get('greeting');
        return ui.doc(ui.stack({ gap: 'md' }, [
          ui.pageHeader({ title: 'Bonjour ' + (viewer?.name ?? '') }),
          ui.text({ markdown: String(row?.value ?? 'Aucun message.') }),
          ui.form({ name: 'greet', submit: ui.button({ label: 'Enregistrer', action: 'save', variant: 'primary' }) }, [
            ui.field({ kind: 'text', name: 'value', label: 'Message', required: true }),
          ]),
        ]), { title: 'Accueil' });
      },
    },
  },
  actions: {
    save: async ({ ctx, form }) => {
      await ctx.kv.put('greeting', form?.value);
      return { ...toast('Enregistré.', 'success'), refresh: true };
    },
  },
});

Le module @capibara-dev/sdk est fourni par la plateforme dans la
sandbox (il n’est jamais lu dans votre dépôt) ; le paquet npm du même nom
expose les mêmes signatures pour le typage et vos tests locaux.

Les familles de handlers

Clé de defineApp Déclaration dans le manifeste Ce que reçoit le handler Ce qu’il renvoie
pages contributes.menuEntries[].view { ctx, params, viewer, state } un nœud du Kit ou ui.doc(…)
widgets contributes.widgets[] avec kind: "kit" idem (config du widget dans ctx.install.config) idem (surface widget, ≤ 200 composants)
panels contributes.panels[] (subject: "party") idem + subject: { type, id } = la fiche affichée idem (surface panel)
forms aucune (ouverts par une action openForm) idem + params de l’ouverture idem (surface form, rendu en modal)
siteBlocks contributes.siteBlocks[] + capability ui.block { ctx, params, visitor, state } — params = champs du bloc remplis par l’auteur du site, visitor = membre du site ou null, viewer = null idem (surface siteBlock, ≤ 200 composants)
sitePages contributes.sitePages[] + capability ui.site idem — params.path (sous-chemin), params.query (paramètres d’URL) idem (surface sitePage, ≤ 500 composants)
actions aucune (les boutons du Kit les nomment) { ctx, action, params, form, formName, state, viewer } des effets (ci-dessous)
events events: ["invoice.paid", …] + capability events.subscribe { ctx, event: { id, type, occurredAt, data } } rien (idempotent : un même événement peut être rejoué)
schedules schedules: [{ key, cron }] { ctx, key } rien
http http: ["/stripe-hook"] { ctx, method, path, query, headers, body, json() } { status?, json? } ou { status?, body? }

Un handler de surface peut être une fonction ou un objet { render }. Une
page, un widget ou un panneau doit être déclaré dans le manifeste pour
être rendu (c’est ce que le tenant voit au consentement) ; les formulaires
sont libres.

Le contexte ctx

ctx.… Contenu
tenant { id, slug, name, plan, locale: 'fr' }
viewer { userId, name, permissions[] } — absent (null) en contexte app (événement, planification, HTTP) et sur les surfaces du site public. permissions = droits accordés à l’installation ∩ droits RBAC de la personne.
visitor surfaces du site public (siteBlock/sitePage) : { memberId, name } du membre du site connecté (jamais son e-mail), sinon null. null aussi hors du site.
install { id, version, config } — config = réglages déclarés (settings, secrets déchiffrés pour votre code seul) + champs de config des widgets.
subject { type: 'party', id } pour un panneau, sinon null.
state l’état opaque renvoyé par votre précédent rendu/action (≤ 8 Ko, signé par la plateforme).
kv (alias storage) get / put / delete / list — clé/valeur cloisonné (votre app × l’organisation), capabilities storage.tenant.ro/rw.
data.<collection> get / set / query / count / delete sur les collections déclarées au manifeste (typées, validées, quotas par formule) — capability data.collections.
files put / get / url / list / delete — fichiers privés de l’app (≤ 5 Mo, route gardée) — capability files.private.
mail.send e-mail transactionnel en blocs à des personnes LIÉES à l’organisation (≤ 5, quota par jour) — capability mail.send, cf. Moteurs.
notify.user / permission / admins notification dans Capibara (cloche + push, quota par jour) — capability notify.send.
calendar.project / remove / clear dates de l’app dans le calendrier de l’organisation (source app:<clé>) — capability calendar.project.
search.index / remove / clear fiches de l’app dans la recherche ⌘K — capability search.index.
approvals.request / get / cancel demande de validation à un humain, décision par l’événement approval.decided — capability approvals.request.
fetch(url, init?) HTTP sortant vers les hosts déclarés http.fetch:<host> uniquement ; réponse { status, headers, bodyBase64, truncated }.
log.info / warn / error journal d’app (200 lignes × 500 caractères par interaction), visible du développeur et de l’admin du tenant.
followup(result) après une réponse { ack: true } : pousse le complément (≤ 15 minutes).

Les façades des modules — ctx.capibara

Lire et écrire dans les modules de l’organisation passe par les
façades : await ctx.capibara.crm.contacts.list({ q }),
ctx.capibara.billing.documents.createDraft({ … }), etc. Chaque façade exige
un scope feuille déclaré ET accordé, et s’exécute sous
la personne qui interagit (pages, actions) ou sous le compte de service de
votre app (événements, planifications, HTTP) — droits, journal et notifications
de Capibara compris. Pas de requête libre : le registre est le contrat.

Le protocole d’interaction

Rendu — votre handler renvoie un nœud (ui.stack(…)) ou un document
{ kit: 1, ui, title?, data?, components?, state? }. Les gabarits
({{ chemin }}, each, $if, $ref) sont remplis avec data, puis le
document est validé (schéma strict, bornes de la surface) avant d’être
rendu avec les composants Capibara. Un document invalide n’est jamais
affiché : l’écran dit « L’app n’a pas répondu » et le journal donne la raison.

Action — un bouton { action: 'save', params } ou la soumission d’un
form appelle actions.save. La réponse combine librement :

Effet Signification
ui (+ data, components, title) remplace la surface par ce document
patch: { path, ui } remplace le nœud au chemin (children.1.children.0)
toast: { tone, text } message éphémère
navigate: '/crm/contacts/…' navigation interne (chemin re-sanitisé par la plateforme)
openForm: { key, params?, title? } ouvre forms[key] en modal — jamais sur le site public (dessinez le formulaire dans la surface elle-même)
close: true ferme le formulaire modal
refresh: true re-rend la surface
ack: true « bien reçu » — puis ctx.followup(…) dans les 15 min
state nouvel état opaque (≤ 8 Ko)

Un handler qui lève une exception renvoie une erreur propre ({ ok: false, error }) : l’utilisateur voit un message honnête, le journal garde le détail.

Le site public : blocs et pages d’app

Une app installée peut vivre sur le site public de l’organisation : des
blocs que l’auteur du site pose dans ses pages depuis l’éditeur
(« Ajouter » › « Vos apps »), et des pages entières à l’adresse choisie par
l’organisation (Mon site › Apps). Mêmes handlers, même Kit, même validation —
avec trois différences :

  • Jamais de viewer : personne du personnel n’est connecté sur le site.
    Le visiteur arrive dans visitor — { memberId, name } s’il est connecté à
    son compte membre du site, null sinon. Ne montrez rien de privé, et
    traitez params (champs du bloc, sous-chemin, paramètres d’URL) comme des
    entrées non fiables.
  • Compte de service : votre code s’exécute sous le compte de service de
    l’app (ctx.capibara.* borné aux scopes consentis) — un formulaire public
    peut créer un contact CRM si le scope crm.contacts:write a été accordé.
  • Actions : boutons et formulaires du Kit fonctionnent (route publique,
    rate-limitée) ; ui, patch, toast, refresh, navigate et ack
    s’appliquent ; openForm n’existe pas sur le site (dessinez le formulaire
    dans la surface). Le rendu d’un visiteur anonyme est mis en cache 60 s
    par (installation, surface, paramètres) ; un membre connecté est toujours
    rendu à la demande.

Dans l’éditeur de site, l’aperçu est le vrai rendu de votre bloc (visiteur
anonyme) ; ses actions y sont simulées. Le harnais de test rend ces surfaces
comme la plateforme : renderSurface(app, 'siteBlock', 'promo', t, { params, visitor: { memberId: 'm1', name: 'Zoé' } }).

Événements, planifications, HTTP entrant

  • Événements : déclarez events: ["invoice.paid"] (catalogue fermé,
    cf. Webhooks sortants pour la liste) et la capability
    events.subscribe. La plateforme livre chaque événement du tenant à votre
    handler par son ledger idempotent — sans webhook, sans serveur. Livraison
    au moins une fois : rendez vos handlers idempotents.
  • Planifications : schedules: [{ key: "nightly", cron: "0 3 * * *" }]
    (5 champs, heure de Paris). Intervalle minimum selon le plan du tenant :
    Gratuit 1 h, Pro 15 min, Business 5 min. Une seule exécution en vol ;
    une occurrence manquée n’en déclenche qu’une.
  • HTTP entrant : http: ["/stripe-hook"] expose une URL publique
    /api/apps/<clé>/<jeton>/<chemin> par installation (visible de l’admin du
    tenant dans Modules › Apps › Réglages, carte « Adresses entrantes » ;
    « Régénérer l’adresse » invalide l’ancienne, et une désinstallation aussi).
    Sans session, rate-limitée,
    corps ≤ 256 Ko, réponse JSON ou texte — jamais du Kit ni du HTML.

Limites et journal

  • 15 s par interaction, corps ≤ 256 Ko, réponse ≤ 1 Mo, ≤ 100 appels broker
    par interaction ; un isolate par app, ≤ 8 interactions simultanées,
    disjoncteur (10 échecs en 5 min à ≥ 50 % → 5 min de suspension, visible
    dans le journal). La table complète des bornes et des codes d’erreur vit
    sur Capabilities, bornes et erreurs.
  • Chaque interaction laisse une ligne dans le journal de l’app (kind, clé,
    statut, latence, erreur, vos ctx.log), consultable par l’admin de
    l’organisation (Modules › Apps) et conservée 7 jours.
  • Un isolate pas encore monté (configuration du runtime pas régénérée ou
    pas rechargée) est dit tel quel dans le journal — « L’isolate de test
    « clé@version » n’est pas monté dans le runtime… » — et la plateforme
    redemande sa configuration toute seule : ce n’est jamais une erreur de
    votre code. capibara status nomme l’étape qui bloque (cf. La CLI
    capibara
    , « Si l’isolate ne se monte pas »).
  • Compatibilité : un module { fetch(request, env) } (contrat v1) reste
    exécuté tel quel, mais ne peut plus contribuer de page — passez à
    defineApp + view.

Backend externe — le même code, chez vous

Vous préférez héberger votre logique ? Déclarez au manifeste backend: { url }
(HTTPS, hôte public) à la place de main — les deux sont exclusifs.
Capibara POSTe alors à cette URL les requêtes du protocole ci-dessus,
signées et horodatées, et rend le Kit que vous renvoyez avec les mêmes bornes
et le même disjoncteur ; avec le SDK, createBackend sert le MÊME
defineApp. ctx.capibara passe par l’API publique ; KV, collections,
fichiers, moteurs et followup restent réservés au code hébergé ; les
organisations consentent à l’envoi de leurs interactions vers votre hôte et
la review humaine est obligatoire. Tout est détaillé sur la page dédiée
Backend externe.

Tester

  • En local, sans réseau : le harnais @capibara-dev/sdk/testing
    exécute vos handlers avec un ctx simulé (KV, collections, fichiers,
    fetch et façades par faux déclarés, journal) —
    createTestContext(), renderSurface(), runAction(),
    runEvent(), runSchedule(), runHttp() + assertions sur le Kit
    rendu. Cf. SDK et le test de l’exemple « Météo ».
  • Kit Studio (/dev/kit-studio) : vos documents rendus en direct dans
    chaque surface, avec des données d’exemple ; capibara kit check <fichier.json> applique le même validateur depuis le terminal.
  • Sur une organisation de test : capibara dev --org <slug> bundle et
    déploie à chaque sauvegarde sur une installation de test (isolate dédié à
    votre version de test, journal en direct) — cf. La CLI capibara.
    Depuis la fiche de l’app du portail, l’installation de test d’une version
    exécute son code hébergé dès qu’un bundle existe (déployé par la CLI, ou
    publié).
Le runtime — defineApp · Capibara for Developers