Arquitectura del Backend CRM
Fases 22.0–48. Estado: DESPLEGADO (VPS en vivo, 68+ endpoints, arquitectura modular). Última actualización: 2026-04-28 (Fase 48 — Descomposición de Arquitectura)
Resumen
Browser (:18888)
│
├── Archivos estáticos ──► Nginx ──► Docker frontend (React + Nginx)
│
├── /api/crm/* ────► Nginx ──► Bun Master Bot (:19210)
│ ├── crmAuthMiddleware (HMAC-SHA256)
│ └── shared/routes/router.ts → 17 módulos de dominio
│
├── /api/auth/* ───► Nginx ──► Bun (:19210) → master-bot/routes/auth.ts
├── /api/cli/* ───► Nginx ──► Bun (:19210) → master-bot/routes/cli.ts
├── /api/internal/* ► Bun (:19210, solo loopback) → master-bot/routes/internal.ts
│
├── /api/sse/* ────► Nginx (sin buffer) ──► Bun (:19210)
│
└── /ws/terminal/* ► Nginx (upgrade) ──► Bun (:19210) → master-bot/routes/websocket.ts
Estructura de Módulos (Fase 48)
API Server (master-bot/api-server.ts, 196 líneas) — dispatcher delgado:
master-bot/routes/auth.ts(562) — OAuth, login, registro, 2FA, device codemaster-bot/routes/internal.ts(282) — chat/save, bridge-event, relay, descargas binariasmaster-bot/routes/cli.ts(293) — ARC CLI + integración MCPmaster-bot/routes/websocket.ts(303) — terminal, event-stream, local-bridge
Rutas CRM (shared/routes/router.ts, 512 líneas) — tabla de dispatch para 17 módulos de dominio:
projects.ts(723),workers.ts(636),skills.ts(665),chat.ts(862)sage.ts(757),onboarding.ts(601),files.ts(381),wiki.ts(258)system.ts,pins.ts,learnings.ts,docs.ts,specs.ts,restart.ts,analytics.ts_utils.ts(343) — tipos compartidos, seguridad de rutas, helpers de auth
Flujo de Autenticación
Usuario ──► LoginOverlay (frontend)
│
├── Email/contraseña → POST /api/auth/login
└── OAuth → /api/auth/{google,github} → callback
│
▼
Auth exitosa → token HMAC-SHA256
│
├── payload = { sub: userId, iat, exp: +24h }
├── signature = HMAC-SHA256(base64url(payload), CRM_SECRET)
└── token = base64url(payload) + "." + signature
│
▼
localStorage('crm-token') → Todas las llamadas API: Authorization: Bearer <token>
│
▼
Token vence → 401 → se requiere login
Archivos Clave
| Archivo | Rol |
|---|---|
shared/auth.ts |
Generación/verificación de token, email/OAuth, restablecimiento de contraseña, verificación de email, CORS, middleware |
shared/vault.ts |
Almacenamiento cifrado AES-256-GCM de secretos (CRM_SECRET se crea automáticamente) |
frontend/src/crm/components/LoginOverlay.jsx |
UI de login (email, OAuth, restablecimiento de contraseña, verificación de email) |
Propiedades de Seguridad
- CRM_SECRET: 32 bytes hex aleatorio, cifrado en vault (AES-256-GCM)
- TTL del token: 24 horas, no renovable
- Firma: HMAC-SHA256, codificación base64url (sin padding)
- CORS: Echo de origen, limitado a headers Authorization + Content-Type
- Verificación de email requerida antes del login para cuentas nuevas
- La autenticación básica de nginx permanece como capa exterior de defensa en profundidad
Rutas de CRM API
Todas las rutas bajo /api/crm/ requieren Authorization: Bearer <token>.
GET /api/crm/projects
Lista todos los proyectos registrados con estado de salud en vivo. Incluye master como pseudo-proyecto (DEBT-6).
| Campo | Fuente |
|---|---|
name, type, bot_username |
config/bot_registry.json (master: entrada virtual, type: "system") |
healthy |
Health check HTTP (hijos: puerto del hijo, master: /api/master/health) |
tmux_alive |
tmux has-session -t <name> (master: citadel-master) |
status |
Archivo heartbeat o derivado de health+tmux (master: healthy/degraded/down) |
El master siempre se antepone como primera entrada en el array de respuesta.
GET /api/crm/projects/:name
Tarjeta de detalle completo del proyecto.
| Campo | Fuente |
|---|---|
manifest |
Texto plano de <cwd>/MANIFEST.md |
heartbeat |
JSON de state/heartbeat_<name>.json |
skills[] |
Nombres de archivos <cwd>/skills/*.md (excluyendo _*.md) |
healthy, tmux_alive |
Igual que el endpoint de lista |
GET /api/crm/projects/:name/logs
Tail de logs JSONL estructurados.
| Parámetro | Por defecto | Máx. |
|---|---|---|
lines |
100 | 500 |
category |
dialog |
system, dialog, error |
Fuente: /var/log/citadel/<name>/<category>-YYYY-MM-DD.log
Lee los archivos de log más recientes primero, parsea JSONL, devuelve en orden cronológico.
GET /api/crm/projects/:name/files
Listado de directorio con protección contra path traversal.
| Parámetro | Por defecto | Descripción |
|---|---|---|
path |
/ |
Ruta relativa dentro del cwd del proyecto |
Seguridad: resolve(base, requested) debe startsWith(resolve(base)). Violación → 403.
Entradas de respuesta: { name, type: "file"|"directory", size?, modified }, ordenadas con directorios primero.
GET /api/crm/projects/:name/skills
Skills instalados con atribución de fuente.
| Fuente | Detección |
|---|---|
file |
<cwd>/skills/*.md (excluyendo _*.md) |
manifest |
MANIFEST.md → skills[] + library_skills[] (parse JSON) |
Deduplicado: la fuente de archivo tiene prioridad sobre el manifest.
POST /api/crm/projects/:name/restart (Fase 22.3)
Reiniciar un child bot via watchdog.
| Aspecto | Detalle |
|---|---|
| Método | Solo POST (GET devuelve 405) |
| Auth | Token HMAC-SHA256 (igual que otras rutas CRM) |
| Cooldown | 30 segundos por proyecto (devuelve 429 si es muy frecuente) |
| Mecanismo | Llama a restartChild() desde watchdog.ts |
| Respuesta | { ok: true } o { error: "..." } |
GET /api/crm/projects/:name/metrics (Fase 22.1)
Timeseries de métricas de calidad para gráficos sparkline.
| Parámetro | Por defecto | Descripción |
|---|---|---|
days |
7 | Número de días a agregar |
Fuente: /var/log/citadel/<name>/quality-events.log → conteos de éxito/fallo por día.
WS /ws/terminal/:name (Fase 22.3)
Terminal WebSocket que transmite contenido del panel tmux.
| Aspecto | Detalle |
|---|---|
| Auth | Query param ?token= (verificado via verifyToken().valid) |
| Modo | Solo lectura por defecto; ?mode=interactive habilita send-keys |
| Poll | tmux capture-pane -p -e -t {session} -S -80 cada 200ms |
| Delta | Solo envía cuando el contenido cambia respecto al anterior |
Seguridad: Protección contra Path Traversal (Fase 22.3, DEBT-7)
Todos los endpoints validan nombres de proyectos antes de procesar:
const SAFE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
export function isValidProjectName(name: string): boolean {
return name.length > 0 && name.length <= 64 && SAFE_NAME_RE.test(name);
}
Aplicado en 3 puntos de entrada: routeCrmRequest(), routeSseRequest(), manejador de upgrade WebSocket en bot.ts.
Nombres inválidos → 403 Forbidden.
Integridad de Datos: Escrituras Atómicas (Fase 22.3, DEBT-2)
Las escrituras de registro en onboarding.ts usan un patrón atómico:
async function atomicWriteJson(filePath: string, data: unknown): Promise<void> {
const tmp = `${filePath}.tmp.${process.pid}`;
await Bun.write(tmp, JSON.stringify(data, null, 2));
Bun.spawn(["mv", tmp, filePath]); // rename atómico POSIX
}
Archivos Clave
| Archivo | Rol | Líneas |
|---|---|---|
shared/crm-routes.ts |
55+ handlers + router routeCrmRequest() + isValidProjectName() |
~4800 |
shared/sage.ts |
Sage Worker: runSageAnalysis(), runBenchmark(), juez LLM, pruebas A/B |
~550 |
shared/db.ts |
SQLite SSOT: userQueries, projectQueries, skillQueries, benchmarkQueries, chatQueries | ~600 |
shared/auth.ts |
Generación y verificación de token, CORS, middleware | ~100 |
master-bot/bot.ts |
Bun.serve: router HTTP + manejador de terminal WebSocket | ~1160 |
master-bot/onboarding.ts |
atomicWriteJson() para escrituras de registro |
~850 |
Proxy Nginx
Archivo: infra/nginx/citadel-crm.conf — reemplaza la configuración legacy citadel-os.
Tres upstreams:
| Upstream | Puerto | Servicio |
|---|---|---|
crm_backend |
19210 | Bun Master Bot (CRM API, health) |
phaser_frontend |
18889 | Docker Phaser office (legacy) |
mcp_bridge |
19200 | Docker MCP state-bridge (legacy) |
| Location | Backend | Auth | Configuración especial |
|---|---|---|---|
/ |
phaser_frontend |
basic auth | Docker Phaser frontend |
/api/crm/ |
crm_backend |
token (sin basic) | timeout 30s, keepalive |
/api/master/health |
crm_backend |
ninguna | Health check público |
/api/sse/ |
crm_backend |
token (sin basic) | proxy_buffering off, timeout 1h (Fase 22.1) |
/ws/ |
crm_backend |
token (sin basic) | Upgrade WebSocket, timeout 1h (Fase 22.1) |
/api/ |
mcp_bridge |
ninguna (sin basic) | Legacy state API + SSE |
/health |
mcp_bridge |
ninguna | Health del bridge legacy |
Todas las locations reenvían X-Real-IP, X-Forwarded-For, X-Forwarded-Proto.
Rutas bloqueadas: /.* (dotfiles), /config/, /state/, /scripts/.
Nota: La prioridad de locations de Nginx asegura que /api/crm/ y /api/sse/ coincidan antes del catch-all /api/ (MCP legacy).
Integración en Master Bot
master-bot/bot.ts Bun.serve (:19210):
async fetch(req) {
/api/master/health → público, sin auth
/api/crm/* → preflight CORS → crmAuthMiddleware → routeCrmRequest()
* → 404
}
El router despacha a funciones handler de shared/crm-routes.ts. Los headers CORS se inyectan en cada respuesta CRM (incluidos errores 401).
Resumen de Endpoints (Todas las Fases)
| Fase | Funcionalidad | Endpoint | Estado |
|---|---|---|---|
| 22.0 | Lista de proyectos | GET /api/crm/projects |
DONE |
| 22.0 | Detalle de proyecto | GET /api/crm/projects/:name |
DONE |
| 22.0 | Tail de logs | GET /api/crm/projects/:name/logs |
DONE |
| 22.0 | Listado de archivos | GET /api/crm/projects/:name/files |
DONE |
| 22.0 | Listado de skills | GET /api/crm/projects/:name/skills |
DONE |
| 22.1 | Streaming SSE de logs | GET /api/sse/logs/:name |
DONE |
| 22.1 | Historial de métricas | GET /api/crm/projects/:name/metrics |
DONE |
| 22.3 | Reinicio de proyecto | POST /api/crm/projects/:name/restart |
DONE |
| 22.3 | Terminal WebSocket | WS /ws/terminal/:name |
DONE |
| 24.5 | CRUD de specs | GET/POST /api/crm/projects/:name/specs |
DONE |
| 24.5 | Rol activo | GET/POST /api/crm/projects/:name/active-role |
DONE |
| 25 | Bundle de skills | GET /api/crm/projects/:name/skills-bundle |
DONE |
| 25 | Sincronización de learnings | GET/POST /api/crm/projects/:name/learnings |
DONE |
| 26 | CRUD de workers | GET/POST /api/crm/projects/:name/workers |
DONE |
| 32 | Guardar wiki | PUT /api/crm/projects/:name/wiki/save |
DONE |
| 32 | Guardar/eliminar skills | PUT/DELETE /api/crm/projects/:name/skills/* |
DONE |
| 33 | Configuración de cuenta | GET/PUT /api/crm/account/settings |
DONE |
| 33 | Crear proyecto | POST /api/crm/projects/create |
DONE |
| 34 | CRUD de issues | POST/GET/PUT /api/mcp/issues/:project |
DONE |
| 34 | Wiki MCP | PUT /api/mcp/wiki/:project |
DONE |
| 34 | Sincronización de roadmap | GET/PUT /api/mcp/roadmap/:project |
DONE |
| 34 | Init CLI | GET /api/cli/init/:project/:mode |
DONE |
| 35 | Ingest de log de terminal | POST /api/crm/projects/:name/terminal/log |
DONE |
| 36 | Chat Cloud PM | POST /api/crm/projects/:name/chat |
DONE |
| 36.6 | Generación de skill | POST /api/crm/projects/:name/skills/generate |
DONE |
| 36.7 | Listado de notebooks | GET /api/crm/projects/:name/notebooks |
DONE |
| 40.14 | Roadmap (contenido) | GET /api/mcp/roadmap/:project (+ campo content) | DONE |
Total: 46+ endpoints (REST + SSE + WebSocket + CLI/MCP)
NotebookLM Bridge (Fase 36.3)
Servicio Python FastAPI separado en :19213 (solo localhost). No forma parte de la CRM API de Bun.
| Endpoint | Método | Propósito |
|---|---|---|
/query |
POST | Búsqueda semántica via Google NotebookLM |
/sync |
POST | Encolar fuente para sincronización asíncrona (fire-and-forget) |
/notebooks/init |
POST | Crear notebook para nuevo proyecto |
/health |
GET | Estado de auth, recuento de notebooks, estadísticas de cola |
El CRM de Bun dispara llamadas fire-and-forget al bridge en:
handleUpdateIssue(cierre/apertura de issue) →/synchandleMcpWikiUpdate(guardado de wiki) →/synchandleCreateProject/handleOnboardingSetup→/notebooks/initexecuteAskNotebooklm→/query(timeout 15s, fallback a local)
Deuda Técnica
Ver docs/backlog/technical-debt.md para el registro completo. Elementos clave:
| ID | Título | Estado |
|---|---|---|
| DEBT-2 | Escrituras JSON atómicas | RESUELTO (Fase 22.3) |
| DEBT-3 | Token Docker integrado → auth de sesión | RESUELTO (Fase 31) |
| DEBT-6 | Punto ciego CRM del master bot | RESUELTO (Fase 22.3) |
| DEBT-7 | Defensa contra path traversal | RESUELTO (Fase 22.3) |
Mantenido por Rick (Orquestrador). Actualizado tras la finalización de cada fase.