L'isolation entre organisations est l'invariant structurant de betool. Voici comment il est appliqué, couche par couche.
Au niveau de la base de données
Les tables métier portent une colonne org_id qui rattache chaque ligne à son organisation (le plus souvent contrainte par clé étrangère vers organisation(id)).
Toutes les requêtes serveur passent par des store methods qui filtrent systématiquement par l'org_id de la session courante. Aucun chemin n'expose une requête arbitraire ; aucun client ne peut forcer un autre org_id.
Pour les ressources rares mutualisables (typiquement les comptes LLM partagés au niveau plateforme), une colonne is_shared BOOLEAN NOT NULL DEFAULT FALSE permet l'opt-in explicite. Règles :
- Toute lecture autorisée à
WHERE org_id = %s OR is_shared = TRUE. - Toute mutation d'une row
is_shared = TRUEexige le rôle hyperadmin (opérateur plateforme). - Toute mutation invalide le cache globalement — pas de divergence per-org.
Au niveau du stockage de fichiers
Les fichiers sont rangés dans un stockage objet (disque local ou bucket cloud selon le déploiement), sous un espace propre à chaque organisation :
orgs/
└── <org_id>/
├── files/<canal>/<exchange_id>/<fichier> ← fichiers reçus par les canaux
└── fixtures/ ← fixtures de benchmark
Les clés sont construites côté serveur (nom de fichier assaini, anti path-traversal). Toute lecture d'un fichier au nom d'un utilisateur, d'un tool ou d'un agent passe par une validation qui refuse une clé hors de l'espace orgs/<org_id>/files/ de l'organisation courante.
Au niveau des caches
Les caches applicatifs (par exemple la résolution des providers LLM) sont indexés par organisation. Quand une ressource is_shared = TRUE est mutée, le cache est invalidé globalement — pas de fenêtre où une org verrait l'ancienne valeur.
Au niveau des secrets
Les credentials (clés LLM, tokens d'auth opérateur, secrets webhook) sont stockés côté serveur, rattachés à chaque org. Ils ne sont jamais retournés en clair par l'API : les routes GET n'indiquent que leur présence (par exemple has_api_key: bool).
Au niveau de l'exécution
Quand un pipeline s'exécute :
- Les opérateurs n'ont accès qu'aux comptes externes déclarés dans l'org.
- Les modèles LLM utilisés sont résolus dans l'ordre
own > shared (fallback auto)— toujours. - Les fichiers attachés ne sont lisibles que dans l'espace de stockage de l'org courante.
Si un opérateur tente d'accéder à une ressource hors-périmètre, l'accès est refusé avec une erreur explicite — pas un timeout silencieux.
Test pratique
À chaque ajout d'une nouvelle ressource mutualisable, l'équipe répond aux 4 questions :
- Si une 2ᵉ org backoffice apparaît, mon code se comporte-t-il bien ?
- Si je modifie une row shared, tous les caches per-org sont-ils invalidés ?
- Mes secrets restent strictement serveur-side ?
- Mon GET permet-il aux pickers d'autres orgs de lister la ressource, sans exposer le secret ?
Si une réponse est non, la ressource n'est pas livrée.