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 dansvisitor—{ memberId, name }s’il est connecté à
son compte membre du site,nullsinon. Ne montrez rien de privé, et
traitezparams(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 scopecrm.contacts:writea été accordé. - Actions : boutons et formulaires du Kit fonctionnent (route publique,
rate-limitée) ;ui,patch,toast,refresh,navigateetack
s’appliquent ;openFormn’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, vosctx.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 statusnomme 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 unctxsimulé (KV, collections, fichiers,
fetchet 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é).