--- type: DC asset_id: DC-EL-IntelliBanks-AnalisisMultiEmpresa-v01 version: v01 tipo: DC — Análisis de arquitectura (multi-tenant) status: 🟢 vigente · análisis, no implementación · insumo del ARQ Multi-Empresa + Zero-Trust owner: Alex sherpa: SherpaX Alex ratificador: Victor Heredia intellibank: IB-EL-EmpowerLabs subbank: EQ-EL-Equipo fecha_creacion: 2026-07-07 proposito: > Análisis multi-empresa de IntelliBanks: una dimensión company_id resuelta server-side, storage/Git intocado, carpeta local por empresa como proyección, cambios por endpoint, fases 0-3 y riesgos. Base ya trabajada que el ARQ-EL-IntelliBanks-MultiEmpresa-ZeroTrust-v01 debe reconciliar con el blindaje Zero-Trust (no re-derivar). relacionado: - SP-EL-IntelliBanks-PlanAccionFable5-20260707-v01 - TP-EL-IntelliBanks-MultiEmpresa-ZeroTrust-FableBrief-v01 - DC-EL-IntelliBanks-DescripcionProducto-v01 - RFI-XX-IntelliBanks-BlindajeGobernanzaSync-v01 tags: [DC, intellibanks, multiempresa, multi-tenant, company-id, analisis, arquitectura] --- # Análisis — Intellibanks Multi-Empresa (multi-tenant) > Panorama de arquitectura. Objetivo: que cada archivo y carpeta tenga un `company_id`, que al arrancar se detecte la empresa del usuario logueado y se genere/monte su carpeta `Intellibanks//`, con el fin de soportar más de una empresa en el futuro. > > **Restricciones dadas:** (1) todos los archivos viven en el **mismo repo Git**; (2) el **acceso lo da la base de datos**; (3) mínima fricción con el esquema actual. --- ## 1. Principio rector El cambio se resuelve con **una sola dimensión nueva: `company_id`**, presente en la base de datos y resuelta en el servidor a partir del usuario autenticado. El **storage/Git no se toca** (los archivos siguen en `storage/skills//...`, con `skill_id` único global); la separación entre empresas es **lógica** (columna en BD + filtros), no física. Esto es lo que mantiene la fricción baja: no hay que reorganizar el repositorio ni mover archivos. La carpeta local por empresa (`Intellibanks//`) es solo una **proyección** de ese scoping: el cliente descarga únicamente los datos de la empresa del usuario y los coloca bajo esa carpeta. --- ## 2. Modelo de datos (backend / MySQL) ### 2.1 Tablas nuevas / columnas nuevas | Tabla | Cambio | Notas | |---|---|---| | `companies` (nueva) | `id`, `name`, `slug` (nombre de carpeta seguro), `status`, `created_at` | `slug` = nombre saneado para la carpeta local (ej. `EmpowerLabs`). | | `users` (existente) | **+ `company_id`** (FK a `companies`) | Cada usuario pertenece a una empresa. Es el ancla de todo el scoping. | | `skills` | **+ `company_id`** | Índice `(company_id)` y `(company_id, folder_id)`. | | `skill_folders` | **+ `company_id`** | Índice `(company_id, parent_id)`. | | `skill_versions` | — (hereda vía `skill_id`) | No necesita columna; se scope-a a través del `skill`. | | `skill_activity_logs`, `user_sync_history` | **+ `company_id`** (opcional) | Útil para auditoría/admin por empresa; no crítico en fase 1. | ### 2.2 Migración de datos existentes (backfill) 1. Crear la empresa por defecto: `EmpowerLabs` (id fijo, ej. `1`). 2. `UPDATE users SET company_id = 1;` (todos los usuarios actuales). 3. `UPDATE skills SET company_id = 1; UPDATE skill_folders SET company_id = 1;` 4. Dejar la columna **nullable con default = 1** durante la transición → **todo el sistema actual sigue funcionando idéntico** mientras se despliega por fases. > Con esto, Fase 0 no cambia el comportamiento visible: es solo estructura + backfill. --- ## 3. Resolución de la empresa (seguridad — punto clave) Hoy los endpoints reciben `usuario` como **parámetro del cliente** (no hay sesión server-side estricta). Para multi-tenant hay dos caminos: - **Corto plazo (baja fricción):** el servidor deriva `company_id` haciendo `JOIN` a `users` por el `usuario` recibido (helper `resolve_company_id($usuario)`), y **nunca** confía en un `company_id` que mande el cliente. Consistente con el modelo actual, sin refactor de sesión. - **Endurecimiento (recomendado a mediano plazo):** guardar `company_id` en la **sesión PHP** al hacer login (`signup.php`) y leerlo de ahí. Elimina el riesgo de que un `usuario` falsificado acceda a otra empresa. **Regla de oro:** el `company_id` de escritura/lectura se resuelve **siempre en el servidor**, a partir del usuario autenticado. El cliente jamás lo elige. --- ## 4. Impacto en el BACKEND (por endpoint) Casi todo el cambio backend es **agregar un filtro `WHERE company_id = :cid`** en lecturas y **setear `company_id` al crear** en escrituras. | Endpoint | Cambio | |---|---| | `signup.php` (login) | Devolver `company_id` y `company_name`/`slug` en la respuesta de login. (Y guardarlo en sesión si se hace el endurecimiento.) | | `list_skills.php` | `WHERE company_id = :cid` (skills y carpetas). | | `folders.php` | Al crear carpeta: setear `company_id`. Al listar/actualizar/borrar: filtrar por `company_id`. | | `upload.php` | Al insertar el skill: setear `company_id` (resuelto del usuario). | | `update_skill.php` | Validar que el skill pertenezca al `company_id` del usuario antes de actualizar. | | `delete_skill.php` | Igual: validar pertenencia antes de borrar. | | `search.php` | `WHERE company_id = :cid`. | | **`get_sync_map.php`** | `WHERE company_id = :cid` en skills y carpetas. Prefijar `folder_path` con la carpeta de empresa (o que el cliente la prefije). | | `get_skill_content.php` | Validar pertenencia del skill al `company_id`. | | `admin_api.php` | Todas las acciones (`get_stats`, `list_all`, `get_activity`, `get_duplicates`, `get_sync_history`, `get_file_detail`) filtran por `company_id`. **Duplicados** pasan a ser por-empresa. Un futuro "super-admin" podría ver todas con un selector de empresa. | | `log_sync.php` | Registrar `company_id` en `user_sync_history` (opcional). | | `share.php` | Definir si el compartir cruza empresas o no (ver §7). | | `render.php`, `restaurar.php`, tags | Validar pertenencia por `company_id`. | **Patrón recomendado:** un helper único `require_company($pdo, $usuario)` que resuelve y cachea el `company_id`, usado al inicio de cada endpoint. Minimiza código repetido y centraliza la seguridad. --- ## 5. Impacto en el FRONTEND | Área | Cambio | |---|---| | **Login / AuthService** | Guardar `company_id` y `company_slug` en la sesión (`ai_skill_store_session`) y en `.user_session.json` (para que el motor de sync los conozca). | | **Carpeta raíz local** | La estructura pasa de `Intellibanks/` a `Intellibanks//`. Al arrancar, crear la carpeta de empresa si no existe. | | **Motor de sync (`intellibanks-sync.js`)** | (a) leer `company_slug` de `.user_session.json`; (b) `get_sync_map.php` ya viene scopeado por empresa; (c) prefijar todas las rutas locales con `/`; (d) el mapa/estado (`.intellibanks_sync_map.json`, etc.) pueden quedarse en la raíz por ahora, o moverse dentro de la carpeta de empresa cuando se soporte más de una empresa por máquina (ver §8). | | **Explorer / breadcrumbs** | La carpeta de empresa es la nueva "raíz" visible. Los breadcrumbs arrancan en ``. | | **`selectSkill` / nombrado** | La resolución de rutas (`getSkillFolderPath`, `cleanFileName`) antepone la carpeta de empresa. | | **Admin** | Muestra solo datos de la empresa del usuario (o selector de empresa para super-admin). | | **Previewer / links locales** | La resolución de `data-path`/rutas relativas parte de `Intellibanks//`. | > El cliente **no decide** la empresa: la recibe del login. Solo la usa para (1) nombrar la carpeta local y (2) que el sync sepa dónde colocar los archivos que el servidor ya filtró. --- ## 6. Carpeta local y arranque Flujo propuesto al iniciar la app (Electron `app.on('ready')` + login): 1. El usuario inicia sesión → `signup.php` devuelve `company_slug` (ej. `EmpowerLabs`). 2. La app escribe `company_id`/`company_slug` en `.user_session.json`. 3. El motor de sync toma `WATCH_DIR = ~/Documents/Intellibanks` y trabaja bajo `Intellibanks//`. Crea la carpeta si no existe. 4. `get_sync_map.php` devuelve **solo** los skills/carpetas de esa empresa; el sync los baja bajo la carpeta de empresa. Estructura resultante: ``` ~/Documents/Intellibanks/ ├── EmpowerLabs/ ← empresa del usuario │ ├── IB-EL-EmpowerLabs/ │ ├── IB-XX-Maestro/ │ └── ... └── (futuro) OtraEmpresa/ ← si el usuario cambia de empresa / multi-empresa ``` --- ## 7. Unicidad de nombres y compartir (decisiones abiertas) - **Unicidad de nombres:** hoy la regla es "nombres nunca se repiten" **global**. En multi-empresa pasa a ser **por empresa** (`UNIQUE(company_id, name)` conceptual). Impacta la detección de **duplicados** del admin (ya la scopeas por `company_id`) y las claves del sync (que ya son por ruta, y la ruta ahora incluye la empresa). - **Compartir entre empresas:** decidir si `share.php` permite compartir un archivo/carpeta a un usuario de **otra** empresa. Por defecto: **no** (aislamiento). Si se necesita, sería una excepción explícita a nivel de registro de compartición. - **Assets/plantillas comunes:** si existen recursos que deban ser visibles por todas las empresas (ej. `IB-XX-Maestro`), se puede reservar un `company_id` especial "global" que se una en las lecturas. Decisión de negocio. --- ## 8. Estrategia por fases (fricción mínima) **Fase 0 — Estructura (invisible):** crear `companies`, agregar `company_id` (nullable, default 1), backfill a `EmpowerLabs`. El sistema sigue idéntico. *Cero impacto de usuario.* **Fase 1 — Scoping server-side:** helper `require_company()`, agregar `WHERE company_id` a lecturas y setear en escrituras. Con una sola empresa el comportamiento no cambia, pero el sistema ya es tenant-aware. **Fase 2 — Carpeta de empresa (cliente):** login devuelve empresa; `.user_session.json` la guarda; el sync anida bajo `Intellibanks//`. Migración local: mover el contenido actual dentro de `EmpowerLabs/` una vez (o dejar que el sync lo reconstruya). **Fase 3 — Multi-empresa real:** alta de una segunda empresa, super-admin con selector, mapa de sync por-empresa (mover `.intellibanks_sync_map.json` dentro de cada carpeta de empresa para permitir varias en una misma máquina), y endurecimiento de sesión. Cada fase es desplegable de forma independiente y compatible hacia atrás. --- ## 9. Riesgos y puntos a cuidar - **Fuga entre empresas:** el mayor riesgo. Mitigación: resolver `company_id` **siempre** en servidor y validar pertenencia en cada escritura (update/delete/render/content). - **Migración local:** la primera vez que se anida bajo `/`, hay que mover el árbol actual o dejar que el sync lo re-baje. Debe hacerse con el candado de subidas puesto para no duplicar en la nube. - **Backfill correcto:** ningún `skill`/`folder` debe quedar sin `company_id` (romper con NOT NULL tras el backfill). - **`created_by`/auditoría:** el `company_id` complementa, no reemplaza, la auditoría por usuario existente. - **Rendimiento:** los nuevos índices `(company_id, ...)` mantienen las consultas rápidas; `get_sync_map.php` ya es de 1 petición. - **Compatibilidad Mac/Windows:** el slug de empresa debe ser seguro como nombre de carpeta en ambos SO (misma sanitización que `cleanFileName`). --- ## 10. Resumen — qué mover y dónde **Base de datos:** tabla `companies`; columna `company_id` en `users`, `skills`, `skill_folders` (y opcional en logs/sync_history). Backfill a `EmpowerLabs`. **Backend (PHP 5.6):** helper `require_company()`; filtro `WHERE company_id` en todas las lecturas; set de `company_id` en las escrituras; validación de pertenencia en update/delete/content/render; `signup.php` devuelve la empresa; admin y duplicados por empresa. **Frontend (Angular + Electron):** login guarda empresa en sesión y en `.user_session.json`; carpeta local pasa a `Intellibanks//`; el sync prefija rutas con la empresa y crea su carpeta al arrancar; explorer/breadcrumbs/previewer parten de esa raíz; admin scopeado. **Storage/Git:** **sin cambios** (archivos por `skill_id`, acceso controlado por BD). ← Esta es la clave de la baja fricción. --- *Documento de análisis. No incluye implementación. Rama `alex`.*