Backend
NestJS 11 en un solo proceso, arrancado desde
backend/src/main.ts con prefijo
global /api.
Módulos
| Módulo | Responsabilidad |
|---|---|
channels/ | Webhooks entrantes y envío saliente de los 7 canales |
bots/ | Orquestación con LangGraph, tools y registro de bots |
modules/scheduling/ | Agenda: huecos, citas, recursos, disponibilidad |
modules/health/ | Pacientes y documentos clínicos. Mínimo a propósito: la lógica clínica vive en funciones auditadas de Postgres |
data-sources/ | Fuentes de conocimiento: documentos, filas, APIs |
embeddings/ | Vectores para la búsqueda semántica del catálogo |
contacts/ | Resolución de contactos entre canales. Global |
outbox/ | Cola de salida con planificador |
reminders/ | Recordatorios de cita programados |
usage/ | Medición de tokens de LLM |
billing/ | Sincronización con Odoo |
payments/ | Place to Pay |
modules/ | Activación de módulos por tenant |
sandbox-channels/ | Modo de canales compartidos para pruebas |
data-sync/ | Copia de producción a test, solo superadmin |
common/queue/, common/cache/, common/rate-limit/ | Infraestructura: BullMQ, caché del bot en Redis, límite de peticiones |
Cuatro de ellos no exponen HTTP: contacts, reminders, outbox y usage son servicios y
planificadores.
Validación
No hay class-validator ni ValidationPipe global. Los DTO son clases de TypeScript planas y
varios controladores tipan el cuerpo en línea. En la práctica significa que la validación de
entrada es responsabilidad de cada handler: no des por hecho que un campo llegó con el tipo que
dice la firma.
Es la deuda técnica más visible del backend y el sitio natural por donde empezar si alguien quiere mejorar la robustez.
Colas
| Cola | Productor | Consumidor |
|---|---|---|
inbound | X, email, webchat, Meta | Ninguno en este repositorio |
data-sync | Endpoint de superadmin | DataSyncProcessor |
product-embeddings | CRM al guardar productos | ProductEmbeddingsProcessor |
Lo de inbound está explicado en Visión general.
Guards
No hay guard global. Los que existen se aplican endpoint por endpoint:
PermissionGuard— permiso concreto del rol, o pertenencia al tenant. Identifica a quien llama por su Bearer token. Un superadmin pasa sin membresía (se resuelve conuser_is_superadmin(p_user_id)sobre el usuario del token; impersonando, el token es el del impersonado y manda su rol). Cubre también/api/modules, que antes no autenticaba.ModuleGuard— el módulo tiene que estar activo para ese tenant.SuperadminGuard— solo administración de plataforma.SandboxModeGuard— solo cuandoCHANNEL_MODE=sandbox.
Qué guard lleva cada endpoint se ve en la Referencia de API.
Acceso a datos
Hay dos clientes de Supabase conviviendo, herencia de la unificación de dos backends:
SupabaseModule, con el tokenSUPABASE_CLIENT.SupabaseService, endatabase/supabase/.
Los dos usan la clave de servicio y por tanto se saltan RLS. Es intencionado —un webhook de Meta no tiene sesión de usuario— pero implica que cualquier consulta del backend tiene que filtrar por tenant explícitamente. Aquí no hay red de seguridad.
Aparte, data-sync abre conexiones de pg directas a las dos bases con
PROD_MIGRATION_DATABASE_URL y DEV_MIGRATION_DATABASE_URL.
Endpoints por módulo
Bots
| Método | Ruta | Qué hace | Guards | Código |
|---|---|---|---|---|
| GET | /api/bots/:botId/flow-versions | sin describir | PermissionGuard | FlowVersions |
| POST | /api/bots/:botId/flow-versions | sin describir | PermissionGuard | FlowVersions |
| DELETE | /api/bots/:botId/flow-versions/:versionId | sin describir | PermissionGuard | FlowVersions |
| GET | /api/bots/:botId/flow-versions/:versionId | sin describir | PermissionGuard | FlowVersions |
| POST | /api/bots/:botId/flow-versions/:versionId/activate | sin describir | PermissionGuard | FlowVersions |
| GET | /api/bots/agent-tools | Static manifest of all tools the flow editor can offer in the "Herramientas del agente" picker on LLM nodes. Per-bot runtime availability (module enabled, tools_config, indexed records…) is enforced server-side at agent invocation time, so this list is intentionally global. Every service a factory might need must be passed here. A factory whose dependency is missing returns null and its tool silently disappears from the picker — which is how the scheduling and catalog tools were invisible even though they worked at runtime. | PermissionGuard | Bots |
| POST | /api/bots/approvals/:id/continue | sin describir | PermissionGuard | Approvals |
| GET | /api/bots/conversations/:conversationId/state | Dónde está la conversación y por qué pasos pasó. Solo estructura, sin PHI. | PermissionGuard | ConversationState |
| GET | /api/bots/conversations/:conversationId/state/history | Puntos de guardado de la conversación (uno por mensaje). Solo estructura. | PermissionGuard | ConversationState |
| POST | /api/bots/conversations/:conversationId/state/replay | Reproduce EN SECO un mensaje desde un punto de guardado: devuelve el recorrido que haría hoy el flujo, sin enviar nada ni tocar la conversación. Es una herramienta de depuración, por eso pide bot.update. | PermissionGuard | ConversationState |
| POST | /api/bots/conversations/:conversationId/state/reset | Olvida la memoria del bot en esta conversación (el próximo mensaje empieza de cero). | PermissionGuard | ConversationState |
| POST | /api/bots/dashboards/generate | sin describir | ninguno | Dashboards |
| POST | /api/bots/dashboards/sessions | sin describir | ninguno | Dashboards |
| GET | /api/bots/dashboards/sessions/:sessionId | sin describir | ninguno | Dashboards |
| POST | /api/bots/dashboards/sessions/:sessionId/files | sin describir | ninguno | Dashboards |
| POST | /api/bots/dashboards/sessions/:sessionId/message | sin describir | ninguno | Dashboards |
| POST | /api/bots/message | sin describir | PermissionGuard | Bots |
| POST | /api/bots/reload | sin describir | PermissionGuard | Bots |
| GET | /api/bots/tenants | Qué tenants tiene cargados el registro: dato de operación, solo superadmin. | PermissionGuard | Bots |
| GET | /api/bots/tenants/:tenantId | sin describir | PermissionGuard | Bots |
| GET | /api/subflows | sin describir | PermissionGuard | Subflows |
| POST | /api/subflows | sin describir | PermissionGuard | Subflows |
| DELETE | /api/subflows/:id | sin describir | PermissionGuard | Subflows |
| GET | /api/subflows/:id | sin describir | PermissionGuard | Subflows |
| PATCH | /api/subflows/:id | sin describir | PermissionGuard | Subflows |
| GET | /api/subflows/:id/usages | sin describir | PermissionGuard | Subflows |
| GET | /api/subflows/:id/versions | sin describir | PermissionGuard | Subflows |
| POST | /api/subflows/:id/versions | sin describir | PermissionGuard | Subflows |
| GET | /api/subflows/:id/versions/:versionId | sin describir | PermissionGuard | Subflows |
29 endpoints.
Agenda
| Método | Ruta | Qué hace | Guards | Código |
|---|---|---|---|---|
| GET | /api/scheduling/appointments | Citas de un rango con los nombres resueltos. Alimenta el calendario. | ModuleGuard | Scheduling |
| POST | /api/scheduling/appointments | sin describir | ModuleGuardAppointmentWriteGuard | Scheduling |
| POST | /api/scheduling/appointments/cancel | sin describir | ModuleGuardAppointmentWriteGuard | Scheduling |
| POST | /api/scheduling/appointments/contact | Asignar o quitar el paciente de una cita existente. | ModuleGuardAppointmentWriteGuard | Scheduling |
| POST | /api/scheduling/appointments/offer-reschedule | Cancela cada cita listada y le ofrece al paciente reagendar por WhatsApp. Se llama desde la alerta de conflicto al guardar un bloqueo de agenda. El body solo trae `resource_id` (no un `appointment_id` suelto): con eso `AppointmentWriteGuard` toma la vía "alta" y comprueba que quien llama sea dueño de ESE recurso, que es exactamente lo que hace falta —todas las citas de la lista son, por construcción, de ese mismo recurso—. Las citas se recargan de la base por id: no se confía en los datos que mande el cliente. | ModuleGuardAppointmentWriteGuard | Scheduling |
| POST | /api/scheduling/appointments/reschedule | sin describir | ModuleGuardAppointmentWriteGuard | Scheduling |
| POST | /api/scheduling/appointments/send-reminder | Envía ya el recordatorio de una cita, a mano. Sale por el mismo canal por el que se reservó (la conversación de origen decide la ruta). Lleva `AppointmentWriteGuard`: avisar a un paciente es actuar sobre su cita, y el guard saca el tenant de la fila real y exige `manage` (con alcance propio si no hay `view_all`). | ModuleGuardAppointmentWriteGuard | Scheduling |
| POST | /api/scheduling/appointments/status | Confirmar, completar, marcar ausencia o reabrir. | ModuleGuardAppointmentWriteGuard | Scheduling |
| POST | /api/scheduling/appointments/substatus | Poner o quitar el sub-estado de una cita (etiqueta informativa). Mismo guard que el estado: quien no puede tocar esa agenda, tampoco etiqueta. | ModuleGuardAppointmentWriteGuard | Scheduling |
| GET | /api/scheduling/config/:entity | Lista entidades de configuración del tenant. Query opcionales: `resource_id`, `location_id` (eq). Para `availabilityExceptions` también `from`/`to` (solape) y `resource_ids` (lista separada por comas) — una sola petición para el calendario. | ModuleGuardPermissionGuard | SchedulingConfig |
| POST | /api/scheduling/config/:entity | sin describir | ModuleGuardPermissionGuardSchedulingWriteGuard | SchedulingConfig |
| DELETE | /api/scheduling/config/:entity/:id | sin describir | ModuleGuardPermissionGuardSchedulingWriteGuard | SchedulingConfig |
| PATCH | /api/scheduling/config/:entity/:id | sin describir | ModuleGuardPermissionGuardSchedulingWriteGuard | SchedulingConfig |
| GET | /api/scheduling/config/availability-exceptions/conflicts | Citas que chocarían con un bloqueo de agenda antes de guardarlo. Ruta de dos segmentos (`availability-exceptions/conflicts`), así que no colisiona con el comodín `:entity` de una sola palabra — pero se declara aquí arriba igualmente, junto al resto de rutas específicas, por consistencia con ellas. | ModuleGuardPermissionGuard | SchedulingConfig |
| GET | /api/scheduling/config/block-reschedule-offer | sin describir | ModuleGuardPermissionGuard | SchedulingConfig |
| PUT | /api/scheduling/config/block-reschedule-offer | sin describir | ModuleGuardPermissionGuard | SchedulingConfig |
| GET | /api/scheduling/config/calendar-view | sin describir | ModuleGuardPermissionGuard | SchedulingConfig |
| PUT | /api/scheduling/config/calendar-view | sin describir | ModuleGuardPermissionGuard | SchedulingConfig |
| GET | /api/scheduling/config/linkable-users | Usuarios vinculables a un recurso. Va antes de :entity para no colisionar. | ModuleGuardPermissionGuard | SchedulingConfig |
| GET | /api/scheduling/config/reminders | sin describir | ModuleGuardPermissionGuard | SchedulingConfig |
| PUT | /api/scheduling/config/reminders | Cambiar cuándo se avisa a los pacientes es configuración de la sede, no mantenimiento de la ficha de un profesional: exige el permiso completo, no la vía "propio" de SchedulingWriteGuard. | ModuleGuardPermissionGuard | SchedulingConfig |
| PUT | /api/scheduling/config/reminders/cold-outreach | sin describir | ModuleGuardPermissionGuard | SchedulingConfig |
| GET | /api/scheduling/config/reminders/outreach | sin describir | ModuleGuardPermissionGuard | SchedulingConfig |
| POST | /api/scheduling/config/reminders/whatsapp-template | sin describir | ModuleGuardPermissionGuard | SchedulingConfig |
| PUT | /api/scheduling/config/reminders/whatsapp-template | sin describir | ModuleGuardPermissionGuard | SchedulingConfig |
| GET | /api/scheduling/config/reminders/whatsapp-template/candidates | sin describir | ModuleGuardPermissionGuard | SchedulingConfig |
| POST | /api/scheduling/config/reminders/whatsapp-template/refresh | sin describir | ModuleGuardPermissionGuard | SchedulingConfig |
| GET | /api/scheduling/config/resources/:id/services | sin describir | ModuleGuardPermissionGuard | SchedulingConfig |
| POST | /api/scheduling/config/resources/:id/services | sin describir | ModuleGuardPermissionGuardSchedulingWriteGuard | SchedulingConfig |
| GET | /api/scheduling/config/services/:id/locations | sin describir | ModuleGuardPermissionGuard | SchedulingConfig |
| POST | /api/scheduling/config/services/:id/locations | sin describir | ModuleGuardPermissionGuardSchedulingWriteGuard | SchedulingConfig |
| GET | /api/scheduling/config/status-colors | Color de cada estado de cita. Lo lee cualquiera del tenant: se pinta en todas las vistas. | ModuleGuardPermissionGuard | SchedulingConfig |
| PUT | /api/scheduling/config/status-colors | sin describir | ModuleGuardPermissionGuard | SchedulingConfig |
| GET | /api/scheduling/config/substatuses | Sub-estados de cita (etiquetas con color encima del estado). Los lee cualquiera del tenant: se pintan en calendario, citas y hoy. | ModuleGuardPermissionGuard | SchedulingConfig |
| PUT | /api/scheduling/config/substatuses | sin describir | ModuleGuardPermissionGuard | SchedulingConfig |
| GET | /api/scheduling/config/substatuses/usage | Uso por sub-estado y estado: solo para quien configura (avisa antes de romper). | ModuleGuardPermissionGuard | SchedulingConfig |
| GET | /api/scheduling/contacts/:contactId/appointments | sin describir | ModuleGuard | Scheduling |
| GET | /api/scheduling/slots | Huecos disponibles. `location_id` es opcional a propósito: sin él busca en todas las sedes y cada hueco viene etiquetado con la suya. | ModuleGuard | Scheduling |
38 endpoints.
Fuentes de datos
| Método | Ruta | Qué hace | Guards | Código |
|---|---|---|---|---|
| GET | /api/data-sources | sin describir | ninguno | DataSources |
| DELETE | /api/data-sources/:id | sin describir | ninguno | DataSources |
| GET | /api/data-sources/:id | sin describir | ninguno | DataSources |
| PATCH | /api/data-sources/:id | sin describir | ninguno | DataSources |
| DELETE | /api/data-sources/:id/node-attach | sin describir | ninguno | DataSources |
| POST | /api/data-sources/:id/node-attach | sin describir | ninguno | DataSources |
| POST | /api/data-sources/:id/reindex | sin describir | ninguno | DataSources |
| GET | /api/data-sources/:id/signed-url | sin describir | ninguno | DataSources |
| POST | /api/data-sources/api | sin describir | ninguno | DataSources |
| POST | /api/data-sources/document | sin describir | ninguno | DataSources |
| POST | /api/data-sources/rows | sin describir | ninguno | DataSources |
11 endpoints.
El resto está en la Referencia de API.