Doc 01 Arquitectura - ronalc90/koptup GitHub Wiki

Arquitectura

Resumen. KopTup es un monorepo con dos aplicaciones: la web (Next.js 14, desplegada en Vercel) y la API (Express, desplegada en Railway). La API guarda los datos en MongoDB, usa Redis para sesiones, cupos y topes de gasto de IA, y habla con OpenAI, con un servidor de correo (SMTP) y con WhatsApp. Un middleware de Next verifica la sesión y el acceso a las demos antes de mostrar una página, y la API vuelve a verificar cada petición.

Para quién: sobre todo para un desarrollador. El dueño puede quedarse con el diagrama general y la tabla qué hace cada pieza.

Índice

  1. El sistema en una imagen
  2. Qué hace cada pieza
  3. Estructura del monorepo
  4. Cómo viaja una petición (5 recorridos reales)
  5. Sesión e inicio de sesión
  6. Dónde vive cada dato
  7. Idiomas: cómo se construyen los mensajes
  8. Arranque del backend, jobs y tareas programadas
  9. Tecnologías y versiones
  10. Protecciones generales
  11. Para desarrolladores: archivos clave
  12. Limitaciones conocidas
  13. Páginas relacionadas

1. El sistema en una imagen

flowchart LR
    classDef visitante fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
    classDef sistema fill:#e0e7ff,stroke:#4f46e5,color:#312e81
    classDef datos fill:#fef3c7,stroke:#d97706,color:#78350f
    classDef externo fill:#f1f5f9,stroke:#64748b,color:#334155

    subgraph NAV["Navegador"]
        UI["Páginas y demos en React"]:::visitante
        LS["localStorage y cookies: sesión, idioma, consentimiento y datos de las demos"]:::visitante
    end

    subgraph VERCEL["Vercel: web Next.js 14"]
        MW["Middleware: /admin, /dashboard, /liquidacion y /demo/slug"]:::sistema
        SSR["Render en el servidor: App Router, metadatos SEO, sitemap"]:::sistema
        NAPI["Rutas API de Next: /api/linkedin-ads/generate y proxies /api/chatbot"]:::sistema
        PUB["Archivos públicos: robots.txt, widget.js, llms.txt, manifest"]:::sistema
    end

    subgraph RAILWAY["Railway: API Express"]
        API["Rutas /api: auth, demos, contacto, chatbot, demo-rag, admin y más"]:::sistema
        JOB["Job de accesos a demos: cada hora"]:::sistema
        MEM["Memoria: documentos de Prueba con tu documento, 1 hora"]:::sistema
        DISK["Disco efímero: estado del chatbot y archivos subidos"]:::sistema
    end

    subgraph DATOS["Datos"]
        DB[("MongoDB")]:::datos
        RD[("Redis")]:::datos
    end

    subgraph EXT["Servicios externos de la API"]
        OAI["OpenAI"]:::externo
        SMTP["Correo: Resend (SMTP solo de respaldo)"]:::externo
        WA["WhatsApp: Cloud API de Meta, plantilla koptup_aviso"]:::externo
        ANT["Anthropic: solo reglas de /liquidacion"]:::externo
        S3["AWS S3: solo /api/documents"]:::externo
        GOO["Google OAuth: opcional"]:::externo
    end

    TAGS["Google tag y LinkedIn Insight: solo con consentimiento"]:::externo
    GH["GitHub: código, CI y wiki"]:::externo

    UI --> SSR
    UI --> MW
    UI --> NAPI
    UI -->|"fetch o axios con Bearer"| API
    UI -.-> TAGS
    MW -->|"verifica sesión y acceso"| API
    NAPI --> API
    API --- MEM
    API --- DISK
    API --> DATOS
    JOB --> DATOS
    API --> EXT
    JOB -->|"recordatorios"| SMTP
    GH -->|"despliegue desde main"| VERCEL
    GH -->|"despliegue desde main"| RAILWAY
Loading

Morado: lo que corre en los servidores de KopTup. Ámbar: dónde se guardan los datos. Gris: servicios externos. La línea punteada solo existe si el visitante acepta las cookies de analítica o marketing.

Puntos clave para el dueño:

  • Dos servidores, un solo dominio para el visitante. El visitante siempre está en www.koptup.com (Vercel). Las acciones que guardan o consultan datos van a la API en Railway.
  • La web no guarda datos de negocio. Cuentas, solicitudes de demo, accesos, contactos y pedidos viven en MongoDB, detrás de la API.
  • Las demos son, en su mayoría, del lado del navegador. 20 de las 28 demos funcionan solo en el navegador con datos de ejemplo. Ocho hablan con la API: el chatbot RAG (incluida "Prueba con tu documento"), el gestor de contenido, LinkedIn Ads, cuentas médicas y el sistema experto, más tres que usan el chatbot RAG para su asistente de IA (mesa de ayuda, talento humano y LMS). Detalle en la Guía de demos.
  • Si la API no responde, las páginas públicas y las demos abiertas siguen funcionando, pero el panel, el portal y las demos con acceso controlado no se abren (falla cerrada, ver §4.2).

2. Qué hace cada pieza

Pieza Dónde corre Qué hace Si falla…
Web Next.js (apps/web) Vercel, región iad1 Renderiza todas las páginas en el servidor (App Router), sirve las demos, el sitemap, el robots.txt y la imagen Open Graph. Aplica cabeceras de seguridad (CSP, HSTS). El sitio no carga.
Middleware de Next (apps/web/src/middleware.ts) Vercel (Edge) Antes de servir /admin, /dashboard, /liquidacion y /demo/<slug>, pregunta a la API por la sesión y el acceso. Renueva el token si venció. Ver la fila de la API.
API Express (apps/backend) Railway (Nixpacks, npm run start) Autenticación, sistema de demos (solicitudes, accesos, catálogo), contacto, chatbot RAG, "Prueba con tu documento", panel de administración, portal del cliente y herramientas internas. Cada ruta tiene su política de autorización. Las páginas protegidas muestran "No pudimos verificar tu sesión" (503) y las demos con acceso muestran "no disponible". Las públicas siguen.
Job de accesos a demos Dentro del proceso de la API Cada hora marca como vencidos los accesos y envía el recordatorio 3 días antes (si hay correo configurado). La vigencia igual se respeta: la API compara la fecha en cada consulta.
Jobs de avisos al dueño Dentro del proceso de la API Cada 5 minutos reintenta los avisos de contactos que fallaron (hasta 6 intentos en 3 días) y cada hora revisa si toca la prueba mensual por correo y WhatsApp. Los avisos nuevos salen igual; solo se pierden los reintentos y la prueba mensual.
MongoDB Servicio administrado (fuera de Railway) Base de datos principal: cuentas, demos, contactos, pedidos, facturas, entregables, mensajes y datos de las herramientas médicas. /health responde 503. Login, panel y demos con acceso no funcionan; las demos publico siguen abiertas.
Redis Plugin de Railway Refresh tokens (7 días), cupos por IP o por cuenta, gasto mensual de IA y candado del job. No se puede iniciar sesión; las funciones de IA públicas se apagan (falla cerrada); el job corre sin candado.
OpenAI Externo Respuestas del chatbot RAG, "Prueba con tu documento", LinkedIn Ads y el asistente del gestor de contenido. Modelo por defecto gpt-4o-mini. El chatbot responde en modo extractivo (cita los fragmentos); las otras funciones de IA muestran un aviso honesto.
Correo (Resend) Externo, por API HTTPS (RESEND_API_KEY); SMTP solo como respaldo, porque Railway Free, Trial y Hobby bloquean el SMTP saliente Aviso al dueño de cada contacto, acuse de la solicitud, aprobación con enlace de activación, recordatorios y restablecer contraseña. Nada se rompe: el panel muestra el enlace de activación para copiarlo o enviarlo por WhatsApp, y el contacto queda guardado con su aviso «Falló» o «No configurado» para reintentarlo.
WhatsApp (Meta) WhatsApp Cloud API con la plantilla aprobada koptup_aviso Aviso al número del dueño por cada contacto, lead de la demo RAG y solicitud de demo. Meta confirma la entrega por un webhook firmado (/api/webhooks/whatsapp), si está configurado. El lead igual queda guardado en MongoDB y el job reintenta el aviso.
AWS S3 Externo Solo para los archivos de /api/documents (API de documentos con sesión). Solo afecta esa API.
Anthropic Externo Solo en las reglas de facturación de la herramienta interna /liquidacion. Solo afecta esa herramienta.
GitHub Externo Código, CI (.github/workflows/ci.yml) y publicación de esta wiki (wiki-sync.yml). Vercel y Railway despliegan desde main con sus propias integraciones. No se publica ni se despliega.

Cuando la API no responde, el middleware contesta /admin, /dashboard y /liquidacion con una página mínima "No pudimos verificar tu sesión" (503, falla cerrada). La captura está en Manual del administrador §15.4.


3. Estructura del monorepo

El repositorio usa npm workspaces (apps/*, packages/*) y Turborepo para correr dev, build, lint, typecheck y test en las dos aplicaciones.

koptup/
├── apps/
│   ├── web/                         ← Web Next.js 14 (Vercel)
│   │   ├── src/
│   │   │   ├── app/                 ← Rutas (App Router): páginas, layouts, rutas API, sitemap, OG
│   │   │   │   ├── demo/<slug>/     ← Las 28 demos (cada una con su layout de SEO)
│   │   │   │   ├── admin/           ← Panel de administración
│   │   │   │   ├── dashboard/       ← Portal del prospecto y del cliente
│   │   │   │   └── api/             ← Rutas API de Next (proxy de LinkedIn Ads y proxies heredados)
│   │   │   ├── middleware.ts        ← Guardia de sesión, rol y acceso a demos
│   │   │   ├── components/          ← Menú, pie, hub de demos, panel, portal, consentimiento, RAG…
│   │   │   ├── lib/                 ← Cliente de la API, roles, catálogo de demos, SEO, analítica
│   │   │   ├── i18n/                ← Configuración de next-intl (idioma por cookie)
│   │   │   └── __tests__/           ← Pruebas Jest de la web
│   │   ├── messages/                ← Textos es/en (generales, por demo y por oferta)
│   │   ├── e2e/                     ← Suite Playwright (9 especificaciones) y mock de OpenAI
│   │   ├── public/                  ← robots.txt, widget.js, llms.txt, íconos, manifest
│   │   ├── scripts/                 ← merge-messages.mjs, check-titles.mjs
│   │   ├── next.config.js           ← Cabeceras de seguridad, CSP, imágenes, next-intl
│   │   └── vercel.json
│   └── backend/                     ← API Express (Railway)
│       ├── src/
│       │   ├── index.ts             ← Arranque: entorno, MongoDB, semillas, servidor y job
│       │   ├── app.ts               ← Middleware global y montaje de todas las rutas
│       │   ├── routes/              ← 26 archivos de rutas + POLICIES.md (política de cada ruta)
│       │   ├── middleware/          ← access.ts (roles y acceso a demos), auth.ts, rate limits
│       │   ├── controllers/         ← Lógica de auth, contacto, pedidos, facturas, chatbot…
│       │   ├── services/            ← 53 servicios: demos, RAG, presupuesto de IA, correo, WhatsApp…
│       │   ├── models/              ← 46 modelos de Mongoose
│       │   ├── jobs/                ← demo-grants.job.ts (vencimientos y recordatorios)
│       │   ├── config/              ← env.ts (validación con zod), demos.ts, mongodb, redis, passport
│       │   ├── data/                ← Semilla del catálogo de demos y estado del chatbot
│       │   └── __tests__/           ← Pruebas unitarias e integración (supertest)
│       ├── railway.json             ← Build, arranque, una instancia y healthcheck /health
│       └── Procfile
├── docs/wiki/                       ← Esta wiki (se publica sola con wiki-sync.yml)
├── .github/workflows/               ← ci.yml (lint, tipos, pruebas, build, e2e, seguridad), monitor.yml y wiki-sync.yml
├── scripts/                         ← audit-check.mjs (npm audit) y uptime-check.sh (monitoreo)
├── docker-compose.dev.yml           ← MongoDB 7 y Redis 7 para desarrollo local
└── turbo.json, package.json         ← Workspaces y tareas de Turborepo (Node ≥ 20.9)

4. Cómo viaja una petición (5 recorridos reales)

Cada diagrama sigue el código de main. Los nombres de las rutas son los reales.

4.1 Carga de una página pública

Ejemplo: alguien abre www.koptup.com/rag. Todas las páginas se renderizan en el servidor en cada visita (el layout raíz lee la cookie de idioma), salvo /icon y /sitemap.xml, que se generan al compilar.

sequenceDiagram
    autonumber
    actor V as Visitante
    participant N as Navegador
    participant W as Vercel Next.js
    participant A as API Railway
    participant D as MongoDB
    participant T as Google tag o LinkedIn

    V->>N: Abre www.koptup.com/rag
    N->>W: GET /rag con la cookie locale
    Note over W: La ruta no está en el matcher del middleware, no se verifica sesión
    W->>W: El layout raíz lee la cookie locale (es por defecto) y carga los mensajes
    W-->>N: HTML con título, descripción, canonical y JSON-LD
    N->>N: React hidrata la página y muestra el aviso de cookies si no hay elección
    N->>A: GET /api/demo-catalog
    A->>D: Lee el catálogo editable de las 28 demos
    A-->>N: slug, nombre, modo de acceso y si está activa
    N->>N: Ajusta las etiquetas Abierta, Requiere acceso o Solo por invitación
    opt Solo si el visitante aceptó analítica o marketing
        N->>T: Carga la etiqueta correspondiente
    end
Loading

Las páginas públicas no dependen de la API para mostrarse: si el catálogo no responde, las etiquetas usan la tabla de respaldo de la web.

Respuestas reales de la API: catálogo de demos y salud

Respuestas reales del entorno de capturas: a la izquierda, 5 de las 28 entradas de GET /api/demo-catalog (lo que recibe la web en los pasos 6 a 8; fuente: "bd" indica que salió de MongoDB y no de la semilla de respaldo); a la derecha, GET /health con MongoDB conectado.

4.2 Abrir una demo con acceso controlado

Ejemplo: un prospecto abre /demo/erp (modo Con solicitud). Las demos en modo Abierta pasan directo tras el paso 3. Las pantallas que ve la persona en cada caso están en Flujos del prospecto y cliente §6.

sequenceDiagram
    autonumber
    actor P as Prospecto
    participant N as Navegador
    participant M as Middleware Next
    participant A as API Railway
    participant D as MongoDB

    P->>N: Abre /demo/erp
    N->>M: GET /demo/erp con las cookies accessToken y refreshToken
    M->>M: Comprueba que erp es una demo conocida
    alt Su copia del catálogo tiene más de 60 s
        M->>A: GET /api/demo-catalog
        A-->>M: Modo de acceso y estado de cada demo
    end
    Note over M: Modo publico y activa: deja pasar sin más consultas
    M->>A: GET /api/demo-access/erp con Authorization Bearer
    alt El token de 15 min venció (401)
        M->>A: POST /api/auth/refresh con el refreshToken
        A-->>M: accessToken nuevo
        M->>A: Repite GET /api/demo-access/erp
    end
    A->>D: Rol vigente, catálogo y DemoGrant de la persona
    A->>D: Si hay acceso vigente, registra la visita (accesos y último ingreso)
    alt Acceso vigente o rol del equipo
        A-->>M: allowed true, reason grant o staff, vencimiento
        M-->>N: La demo, con no-store y noindex, y la cookie renovada si hizo falta
    else Sin acceso, vencido, retirado o sin sesión
        A-->>M: allowed false y el motivo
        M-->>N: Reescribe a /demo-acceso/erp con el motivo (la URL no cambia)
    else La API no responde
        M-->>N: Pantalla de acceso con motivo no_disponible
    end
    opt Si la demo usa una API protegida (cuentas médicas, sistema experto, gestor de contenido, LinkedIn Ads)
        N->>A: Llamada a su API con Bearer
        A->>A: requireStaffOrDemoAccess vuelve a decidir en cada petición
    end
Loading

El middleware es una barrera adicional: la API vuelve a autorizar cada endpoint con el rol leído de la base de datos. Reglas completas en Roles y permisos.

4.3 Envío del formulario de contacto

sequenceDiagram
    autonumber
    actor V as Visitante
    participant N as Navegador en /contact
    participant A as API Railway
    participant D as MongoDB
    participant W as WhatsApp de Meta
    participant S as Correo por Resend

    V->>N: Llena nombre, email, servicio y mensaje (teléfono, empresa y presupuesto opcionales)
    N->>A: POST /api/contact
    A->>A: Cupo estricto de 5 envíos por minuto por IP y validación de campos
    A->>D: Guarda un Contact con origen contact-form, estado new y los avisos pendientes
    A-->>N: success true
    N-->>V: Mensaje de confirmación
    par Avisos al dueño, sin bloquear la respuesta
        A->>W: Plantilla koptup_aviso al número del dueño, si está configurado
    and
        A->>S: Correo al dueño con Responder a la persona, si está configurado
    end
    A->>D: Guarda el resultado de cada canal en notificaciones
    Note over D: El equipo lo ve en Admin, sección Contactos
Loading

El mismo canal (services/lead.service.ts) registra los leads de "Prueba con tu documento" (origen demo-rag) y del formulario "Solicitar demo" (origen demo-request). Recorrido con capturas en Flujos del visitante.

4.4 Pregunta al chatbot RAG (modo "Prueba el asistente")

En /demo/chatbot el visitante elige una de tres empresas ficticias. La primera vez, el navegador crea un bot real en la API y le sube los documentos de esa empresa; el id del bot queda en localStorage para las siguientes visitas.

sequenceDiagram
    autonumber
    actor V as Visitante
    participant N as Navegador en /demo/chatbot
    participant A as API Railway
    participant R as Redis
    participant O as OpenAI
    participant F as MongoDB

    V->>N: Elige una empresa de ejemplo
    alt Primera vez o el bot ya no existe
        N->>A: POST /api/chatbot/bots y POST /api/chatbot/bots/id/docs
        A->>F: Guarda el bot, sus documentos y sus fragmentos
    end
    V->>N: Escribe una pregunta
    N->>A: POST /api/chatbot/bots/id/chat con el mensaje, el historial y el modelo
    A->>A: Busca los 5 fragmentos más parecidos (BM25)
    A->>R: Consulta el gasto del mes (chatbot:spend)
    alt Hay clave de OpenAI y queda presupuesto
        A->>O: Chat Completions con las reglas de citado y los fragmentos
        O-->>A: Respuesta y tokens usados
        A->>R: Suma el costo al gasto del mes
    else Sin clave, sin presupuesto o error del proveedor
        A->>A: Modo extractivo: devuelve los fragmentos citados tal cual
    end
    A->>F: Guarda la conversación (últimos 200 turnos por bot)
    A-->>N: Respuesta, fuentes, confianza, latencia, modelo y tokens
    N-->>V: Respuesta con citas numeradas y panel de fuentes
Loading

Si los fragmentos no contienen la respuesta, el bot contesta "No encontré esa información en los documentos cargados." Las demos de mesa de ayuda, talento humano y LMS usan este mismo recorrido para su asistente de IA, cada una con su propio bot. Guía de uso con capturas en Guía de la demo del chatbot.

4.5 Subida de un documento en "Prueba con tu documento"

sequenceDiagram
    autonumber
    actor V as Visitante
    participant N as Navegador
    participant A as API Railway
    participant R as Redis
    participant M as Memoria del backend
    participant D as MongoDB
    participant O as OpenAI

    N->>A: GET /api/demo-rag/status
    A->>R: Redis listo y gasto del mes (demo-rag:spend)
    A-->>N: Habilitada o no, y los límites
    alt Apagada, sin Redis, sin clave o sin presupuesto
        N-->>V: Muestra Agenda una demo con nosotros
    end
    V->>N: Elige un PDF, DOCX o TXT, escribe su email y autoriza el tratamiento de datos
    N->>A: POST /api/demo-rag/documents (archivo, email y consentimiento)
    A->>A: Máximo 10 intentos por IP cada 10 min, formato y 5 MB, todo en memoria
    A->>R: Reserva 1 de los 3 documentos del día para esa IP
    A->>M: Extrae el texto (máx. 30 páginas), lo parte en fragmentos y arma el índice BM25
    A->>D: Registra el lead en Contact con origen demo-rag (y avisa al equipo)
    A-->>N: 201 con el id del documento, páginas y vencimiento en 1 hora
    loop Hasta 10 preguntas por documento
        V->>N: Escribe una pregunta
        N->>A: POST /api/demo-rag/documents/id/questions
        A->>R: Verifica de nuevo el presupuesto del mes
        A->>M: Recupera los 5 fragmentos más parecidos
        A->>O: gpt-4o-mini con las reglas de citado
        A->>R: Suma el gasto del mes
        A-->>N: Respuesta con citas por página o fragmento y preguntas restantes
    end
    Note over M: A la hora, o al pulsar borrar, el documento se elimina de la memoria
Loading

El documento nunca se guarda en disco, MongoDB ni S3. Capturas del recorrido en Flujos del visitante.


5. Sesión e inicio de sesión

Pieza Detalle
Access token JWT de 15 minutos con id, email, role y name. La web lo guarda en la cookie accessToken y lo envía como Authorization: Bearer.
Refresh token JWT de 7 días. Cookie refreshToken en el navegador y copia en Redis (refresh_token:<id>); cerrar sesión o restablecer la contraseña con el enlace la borra.
Renovación El cliente HTTP de la web (lib/api.ts) y el middleware piden POST /api/auth/refresh cuando el access token vence.
Datos del usuario en el navegador localStorage.user (nombre y rol) para pintar los menús. No da permisos: el middleware y la API verifican el rol real.
Google Inicio de sesión con Google si el backend tiene GOOGLE_CLIENT_ID y GOOGLE_CLIENT_SECRET; vuelve por /auth/callback.
Cuentas invitadas Una cuenta creada al aprobar una demo queda en estado invitado hasta que la persona usa el enlace de activación (72 h, un solo uso).

El detalle de quién entra a dónde está en Roles y permisos.


6. Dónde vive cada dato

Dato Dónde Cuánto dura
Cuentas, solicitudes y accesos a demos, catálogo editable, bitácora, contactos, pedidos, facturas, entregables, mensajes, notificaciones, proyectos MongoDB (ver Modelos de datos) Permanente; las solicitudes rechazadas y los prospectos sin actividad se anonimizan a los 24 meses, y Admin › Privacidad suprime a pedido
Refresh tokens Redis 7 días
Gasto de IA del mes (demo-rag, chatbot, linkedin-ads, content) Redis (<función>:spend:AAAA-MM) Por mes (UTC)
Cupos: documentos por IP al día, solicitudes de demo por IP y por email, LinkedIn Ads Redis (las solicitudes tienen respaldo en memoria) De 10 minutos a 1 día
Candado del job de accesos Redis (jobs:demo-grants:lock) 10 minutos
Límites por IP o por cuenta (100 cada 15 min en general y un contador propio en cada ruta de cuenta y en contacto) Memoria de cada instancia La ventana del límite
Documentos de "Prueba con tu documento" Memoria del backend (máx. 200 a la vez) 1 hora
Bots del chatbot y sus documentos MongoDB (chatbot_bots, chatbot_bot_chunks) 12 meses sin uso (los marcados como de ejemplo, 30 días)
Preguntas al widget de un bot de usuario («Prueba el asistente» y la vista previa no se guardan) MongoDB (chatbot_turns, índice TTL) 30 días
Candado del job de conservación de datos Redis (jobs:retention:lock) 30 minutos
Adjuntos de pedidos y archivos de cuentas médicas Disco del backend (uploads/) Hasta el próximo redespliegue
Archivos de /api/documents AWS S3 Permanente
Lo que el visitante crea dentro de las demos (negocios del CRM, ventas del POS…) localStorage del navegador Hasta que la persona borre los datos del navegador
Sesión, idioma y elección de cookies Cookies accessToken, refreshToken, locale y localStorage (user, cookie_preferences) 15 min, 7 días, 1 año y sin vencimiento

7. Idiomas: cómo se construyen los mensajes

El sitio está en español e inglés sin rutas separadas (no hay /es ni /en): el idioma es la cookie locale (español por defecto). Los textos viven en archivos JSON y se agregan antes de compilar.

flowchart LR
    classDef archivo fill:#fef3c7,stroke:#d97706,color:#78350f
    classDef sistema fill:#e0e7ff,stroke:#4f46e5,color:#312e81
    classDef visitante fill:#dbeafe,stroke:#2563eb,color:#1e3a8a

    BASE["messages/es.json y en.json: menú, páginas, panel, portal"]:::archivo
    DEMOS["messages/demos: 56 archivos, uno por demo e idioma"]:::archivo
    OFF["messages/offerings: 64 archivos, ofertas y páginas RAG"]:::archivo
    MERGE["scripts/merge-messages.mjs: corre en predev, prebuild y pretest"]:::sistema
    AGG["_demos.es/en.json y _offerings.es/en.json"]:::archivo
    LAYOUT["Layout raíz: lee la cookie locale y une los tres grupos"]:::sistema
    REQ["i18n/request.ts: lo mismo para componentes de servidor y metadatos"]:::sistema
    CLIENT["NextIntlClientProvider: useTranslations en componentes de cliente"]:::sistema
    BTN["Botón ES / EN del menú: cambia la cookie y recarga"]:::visitante

    DEMOS --> MERGE
    OFF --> MERGE
    MERGE --> AGG
    BASE --> LAYOUT
    AGG --> LAYOUT
    BASE --> REQ
    AGG --> REQ
    LAYOUT --> CLIENT
    BTN --> LAYOUT
Loading

Los archivos agregados (_demos.*, _offerings.*) no se editan a mano: se regeneran con npm run merge-messages y el CI verifica que estén al día.

  • La web carga en cada página todos los mensajes del idioma elegido (generales, de demos y de ofertas).
  • Algunas demos guardan además textos propios en sus componentes; las páginas legales y parte de "Nosotros" no tienen traducción completa (ver Flujos del visitante).
  • La API responde mensajes de error en español; la web traduce los códigos estables (code) cuando corresponde.

8. Arranque del backend, jobs y tareas programadas

Arranque (apps/backend/src/index.ts)

flowchart TD
    classDef sistema fill:#e0e7ff,stroke:#4f46e5,color:#312e81
    classDef decision fill:#fff,stroke:#64748b,color:#334155
    classDef error fill:#fee2e2,stroke:#dc2626,color:#7f1d1d

    A["1. Valida las variables de entorno con zod"]:::sistema
    B{"¿Producción y falta MONGODB_URI o JWT_SECRET?"}:::decision
    X["No arranca"]:::error
    C["2. Crea las carpetas de uploads"]:::sistema
    D["3. Conecta MongoDB (si falla, sigue y /health responde 503)"]:::sistema
    E["4. ADMIN_EMAIL: rol admin solo si no hay ningún otro admin"]:::sistema
    F["5. Siembra las demos que falten en el catálogo (no pisa cambios del panel)"]:::sistema
    G["6. Inicializa Google OAuth si está configurado"]:::sistema
    H["7. Monta la app Express y abre el puerto"]:::sistema
    J["8. Programa los jobs: accesos a demos, conservación<br/>y reintentos y prueba mensual de los avisos"]:::sistema

    A --> B
    B -->|"Sí"| X
    B -->|"No"| C --> D --> E --> F --> G --> H --> J
Loading

Railway da por sano el despliegue cuando GET /health responde 200, es decir, con MongoDB conectado; si no, deja la versión anterior.

Tareas automáticas

Tarea Dónde Cuándo Qué hace
Job de accesos a demos (jobs/demo-grants.job.ts) Proceso del backend Cada hora (DEMO_GRANTS_JOB_INTERVAL_MS), la primera a los ~30 s del arranque Marca expirado los accesos vencidos y envía un recordatorio por persona con los accesos que vencen en 3 días (solo con correo configurado). Toma un candado en Redis para que con varias instancias corra una sola; DEMO_GRANTS_JOB_ENABLED=false lo apaga.
Barrido de documentos de la demo RAG Proceso del backend Cada minuto Borra de la memoria los documentos de "Prueba con tu documento" con más de 1 hora.
Semilla del catálogo de demos Arranque En cada arranque Inserta las demos que falten (idempotente).
Reintentos de avisos (jobs/notify-retry.job.ts) Proceso del backend Cada 5 minutos, la primera ~1 min después del arranque Reenvía los avisos al dueño que fallaron o quedaron pendientes en los contactos de los últimos 3 días (esperas de 5 min a 12 h, hasta 6 intentos), sin tocar los canales que ya salieron. Candado en Redis; NOTIFY_JOBS_ENABLED=false lo apaga.
Prueba mensual de avisos (jobs/notify-heartbeat.job.ts) Proceso del backend Revisa cada hora; envía cada NOTIFY_HEARTBEAT_DAYS días (30), entre 9:00 y 18:59 hora de Bogotá Aviso de prueba por correo y WhatsApp; si uno falla, avisa por el otro. Mantiene viva la plantilla de Meta. 0 o NOTIFY_JOBS_ENABLED=false lo apagan.
CI (ci.yml) GitHub Actions En cada pull request y cada push a main Mensajes i18n al día, lint, tipos, títulos de página, Jest (web y backend con MongoDB 7 y Redis 7), build y la suite Playwright contra la app compilada con un mock de OpenAI. No despliega.
Publicar la wiki (wiki-sync.yml) GitHub Actions En cada push que cambie docs/wiki/ Copia docs/wiki/ a la wiki del repositorio y quita la extensión .md de los enlaces.
Despliegues Integraciones de Vercel y Railway En cada push a main Vercel compila la web; Railway compila y arranca la API.

No hay otras tareas programadas (cron) en main.


9. Tecnologías y versiones

Versión declarada en package.json y versión instalada según package-lock.json de main.

Área Tecnología Declarada Instalada Para qué
Plataforma Node.js >=20.9.0 20 en CI Web y API
Monorepo Turborepo ^1.10.16 1.13.4 Tareas de apps/*
Web Next.js 14.2.35 14.2.35 App Router, middleware, metadatos, imágenes
Web React ^18.2.0 18.3.1 Interfaz
Web next-intl ^3.4.0 3.26.5 Español e inglés
Web Tailwind CSS ^3.3.5 3.4.19 Estilos
Web TypeScript ^5.3.2 5.9.3 Tipos (web y API)
Web axios · SWR · framer-motion ^1.6.2 · ^2.2.4 · ^10.16.5 1.20.0 · 2.3.6 · 10.18.0 Llamadas a la API, caché de datos, animaciones
Web react-hook-form + zod · jsPDF · next-themes ^7.48.2 · ^4.2.1 · ^0.2.1 — Formularios, PDF de las demos, tema claro u oscuro
API Express ^4.18.2 4.22.3 Servidor HTTP
API Mongoose ^8.20.0 8.24.5 MongoDB
API redis (node-redis) ^4.6.11 4.7.1 Redis
API openai ^4.104.0 4.104.0 Chat Completions
API @anthropic-ai/sdk ^0.70.0 0.70.1 Reglas de /liquidacion
API jsonwebtoken · passport-google-oauth20 ^9.0.2 · ^2.0.0 9.0.2 · 2.0.0 Sesiones y Google
API helmet · express-rate-limit · zod ^7.1.0 · ^7.1.5 · ^3.22.4 7.2.0 · 7.5.1 · 3.25.76 Cabeceras, límites por IP, validación
API nodemailer ^10.0.6 10.0.16 Correo por SMTP, solo como respaldo. Resend y la WhatsApp Cloud API de Meta se llaman con fetch (sin SDK); la dependencia twilio se quitó
API multer · pdf-parse · mammoth · exceljs ^2.4.0 · ^1.1.1 · ^1.6.0 · ^4.4.0 multer 2.4.0 Subidas y lectura de PDF, DOCX y Excel
API @aws-sdk/client-s3 ^3.461.0 3.1147.0 Archivos de /api/documents
API winston · swagger-ui-express ^3.11.0 · ^5.0.0 3.18.3 Logs y /api-docs (solo fuera de producción)
Pruebas Jest · Playwright ^29.7.0 · ^1.40.1 29.7.0 · 1.56.1 Unitarias, integración y e2e

10. Protecciones generales

Sin entrar en detalles que faciliten un ataque, estas son las capas que protegen el sistema:

Capa Dónde Qué hace
Cabeceras de seguridad next.config.js y helmet en la API HSTS, nosniff, Referrer-Policy, Permissions-Policy (cámara y micrófono bloqueados, salvo en /demo/telemedicina, que los permite solo al propio sitio) y una CSP que solo abre los dominios de Google o LinkedIn si su variable existe al compilar. Todo el sitio, salvo /embed/*, solo se puede incrustar en el propio dominio.
CORS app.ts La API solo acepta peticiones del navegador desde koptup.com y www.koptup.com (más los orígenes de CORS_ORIGIN).
Límites por IP middleware/rateLimiter.ts y servicios Por defecto, 100 peticiones cada 15 min en general; un contador propio para inicio de sesión (por IP y correo), registro, recuperación y restablecimiento de contraseña, activación, canje del inicio con Google y contacto, con mensajes en español (detalle en API › Límites); 60 por minuto en el chatbot; cupos propios en la demo RAG, en LinkedIn Ads y en las funciones de IA de documentos y de salud.
Topes de gasto de IA services/ai-budget.service.ts Tope mensual en dólares por función (por defecto 50 para la demo RAG y el chatbot, 20 para LinkedIn Ads y el gestor de contenido). Si Redis no responde, no se llama al modelo.
Autorización Middleware de Next, layouts del panel y del portal, y cada ruta de la API Tres capas; la que manda es la API, que lee el rol de la base de datos en cada petición. Ver Roles y permisos.
Variables de entorno config/env.ts En producción la API no arranca sin MONGODB_URI y JWT_SECRET; los mensajes nombran la variable, nunca su valor.

11. Para desarrolladores: archivos clave

Tema Archivo
Montaje de rutas y middleware global apps/backend/src/app.ts
Política de cada ruta apps/backend/src/routes/POLICIES.md
Roles, requireRole, requireStaffOrDemoAccess apps/backend/src/middleware/access.ts
Decisión de acceso a una demo apps/backend/src/services/demo-access.service.ts
Guardia de la web apps/web/src/middleware.ts y apps/web/src/lib/auth-roles.ts
URL de la API en la web apps/web/src/lib/backend-url.ts (NEXT_PUBLIC_API_URL)
Pipeline RAG compartido apps/backend/src/services/rag-pipeline.ts
Demo "Prueba con tu documento" apps/backend/src/services/demo-rag.service.ts y routes/demo-rag.routes.ts
Chatbot (bots, documentos, chat) apps/backend/src/routes/chatbot.routes.ts, services/chatbot-store.service.ts y models/ChatbotBot.ts
Derechos del titular y conservación apps/backend/src/services/privacy.service.ts, services/retention.service.ts y jobs/retention.job.ts
Leads (contacto, demo RAG, solicitud) apps/backend/src/services/lead.service.ts
Job de accesos apps/backend/src/jobs/demo-grants.job.ts
Mensajes e idioma apps/web/src/app/layout.tsx, src/i18n/request.ts, scripts/merge-messages.mjs
Variables de entorno apps/web/.env.example, apps/backend/.env.example y Operación y despliegue

12. Limitaciones conocidas

  • El backend de producción no responde en este momento; ver Estado y próximos pasos.
  • Disco efímero en Railway. Los archivos subidos a uploads/ (adjuntos de pedidos, cuentas médicas) se pierden en cada redespliegue hasta que se monte un volumen en /app/uploads (Operación y despliegue › Acciones pendientes).
  • Una sola instancia de la API, a propósito. Los documentos de "Prueba con tu documento", los límites generales por IP y la caché de sesión de la web (30 s) y del catálogo (60 s) viven en la memoria del proceso; por eso railway.json fija numReplicas: 1. Para escalar a varias instancias hay que pasar ese estado a Redis primero. Un cambio de rol puede tardar hasta 30 s en reflejarse en la web (la API lo aplica de inmediato).
  • En desarrollo: propuestas, anticipo y conversión a cliente no están en main; ver Estado y próximos pasos.

13. Páginas relacionadas

⚠️ **GitHub.com Fallback** ⚠️