Multi-tenancy estricto
El aislamiento entre organizaciones es el invariante fundacional de betool. Así se aplica, capa por capa.
A nivel de base de datos
Las tablas de negocio llevan una columna org_id que vincula cada fila a su organización (casi siempre restringida por una clave foránea a organisation(id)).
Todas las consultas del lado servidor pasan por métodos de store que filtran sistemáticamente por el org_id de la sesión actual. Ningún camino de código expone una consulta arbitraria; ningún cliente puede forzar un org_id distinto.
Para el pequeño número de recursos que pueden compartirse entre organizaciones (normalmente cuentas de LLM compartidas a nivel de plataforma), una columna is_shared BOOLEAN NOT NULL DEFAULT FALSE habilita un opt-in explícito. Reglas:
- Cualquier lectura se autoriza con
WHERE org_id = %s OR is_shared = TRUE. - Cualquier mutación de una fila
is_shared = TRUErequiere el rol de hyperadmin (operador de plataforma). - Cualquier mutación invalida la caché globalmente — sin divergencia por organización.
A nivel de almacenamiento de archivos
Los archivos se guardan en un almacenamiento de objetos (disco local o bucket cloud según el despliegue), bajo un espacio propio de cada organización:
orgs/
└── <org_id>/
├── files/<canal>/<exchange_id>/<archivo> ← archivos recibidos por los canales
└── fixtures/ ← fixtures de benchmark
Las claves se construyen del lado del servidor (nombre de archivo saneado, anti path-traversal). Toda lectura de un archivo en nombre de un usuario, de una herramienta o de un agente pasa por una validación que rechaza una clave fuera del espacio orgs/<org_id>/files/ de la organización actual.
A nivel de caché
Las cachés de la aplicación (por ejemplo, la resolución de los proveedores de LLM) están indexadas por organización. Cuando se muta un recurso is_shared = TRUE, la caché se invalida globalmente — no hay ninguna ventana durante la cual una organización pueda ver un valor obsoleto.
A nivel de secretos
Las credenciales (claves de LLM, tokens de autenticación de operadores, secretos de webhook) se almacenan del lado del servidor, vinculadas a cada organización. Nunca se devuelven en texto plano por la API: las rutas GET solo indican su presencia (por ejemplo has_api_key: bool).
A nivel de ejecución
Cuando se ejecuta un pipeline:
- Los operadores solo tienen acceso a las cuentas externas declaradas dentro de su organización.
- Los modelos de LLM se resuelven en el orden
own > shared (fallback auto)— siempre. - Los archivos adjuntos solo son legibles en el espacio de almacenamiento de la organización actual.
Si un operador intenta acceder a un recurso fuera de su ámbito, el acceso se rechaza con un error explícito — no un timeout silencioso.
Prueba práctica
Cada vez que se añade un nuevo recurso compartible, el equipo responde a estas 4 preguntas:
- Si aparece una segunda organización de backoffice, ¿mi código se comporta correctamente?
- Si muto una fila compartida, ¿se invalidan todas las cachés por organización?
- ¿Mis secretos permanecen estrictamente del lado servidor?
- ¿Mi GET permite que los pickers de otras organizaciones listen el recurso sin exponer el secreto?
Si alguna respuesta es no, el recurso no se despliega.