Skip to content

Doc 11 Operacion y Despliegue

github-actions[bot] edited this page Oct 14, 2026 · 3 revisions

Operación y despliegue

Resumen. El código vive en GitHub. Cada push a main lo despliegan dos plataformas por su cuenta: Vercel construye la web (www.koptup.com) y Railway construye la API. GitHub Actions verifica (lint, tipos, pruebas, una suite de punta a punta con Playwright y un job de seguridad), vigila cada 15 minutos que la web y la API respondan y publica esta wiki; no despliega nada.

Aquí encuentras todas las variables de entorno reales de cada servicio, el paso a paso para que los avisos de nuevos contactos te lleguen por correo y por WhatsApp (4.5), cómo levantar el sistema en tu computador, cómo correr las pruebas, once runbooks (procedimientos paso a paso para cuando algo falla) y lo que el dueño todavía debe configurar en las plataformas. Situación actual (9 de octubre de 2026): la web funciona, pero el backend de Railway responde «Application not found». El runbook 9.1 explica cómo recuperarlo.

Índice

  1. El despliegue en una imagen
  2. Dónde corre cada pieza
  3. Integración continua (CI)
  4. Variables de entorno (y 4.5 Configurar los avisos al dueño)
  5. Levantar todo en local, paso a paso
  6. Pruebas: unitarias, de integración y de punta a punta
  7. Salud del sistema, monitoreo y registros
  8. Cómo se publica esta wiki
  9. Runbooks: qué hacer cuando algo falla
  10. Acciones pendientes del dueño
  11. Limitaciones conocidas
  12. Para desarrolladores: archivos clave
  13. Páginas relacionadas

1. El despliegue en una imagen

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

    DEV["Desarrollador: rama y pull request"]:::admin
    MAIN["GitHub: rama main"]:::externo
    CI["GitHub Actions: CI (solo verifica)"]:::externo
    WIKI["GitHub Actions: Publicar wiki"]:::externo
    MON["GitHub Actions: Monitoreo cada 15 min"]:::externo
    GW["Wiki de GitHub"]:::externo
    VB["Vercel: build de apps/web (next build)"]:::externo
    RB["Railway: build de apps/backend (Nixpacks)"]:::externo
    WEB["www.koptup.com (web Next.js)"]:::sistema
    API["API Express en Railway"]:::error
    DB[("MongoDB")]:::externo
    RD[("Redis")]:::externo
    OA["OpenAI"]:::externo
    MAIL["Resend (correo) y Meta (WhatsApp)"]:::externo
    VIS["Visitante, prospecto, equipo"]:::visitante

    DEV -->|"PR"| CI
    DEV -->|"merge"| MAIN
    MAIN --> CI
    MAIN -->|"cambios en docs/wiki"| WIKI --> GW
    MAIN -->|"integración de Vercel"| VB --> WEB
    MAIN -->|"integración de Railway"| RB --> API
    VIS --> WEB
    MON -->|"GET /"| WEB
    MON -->|"GET /health"| API
    WEB -->|"NEXT_PUBLIC_API_URL"| API
    API --> DB
    API --> RD
    API --> OA
    API --> MAIL
Loading

Un merge a main dispara tres cosas en paralelo: CI (verifica), Vercel (web) y Railway (API). Aparte, el monitoreo revisa cada 15 minutos que la web y la API respondan. La API está en rojo porque hoy no responde.

Tres ideas para quedarse:

  1. CI todavía no es una puerta. Vercel y Railway despliegan main sin esperar a que CI termine, y main no exige CI en verde para fusionar: una prueba en rojo no frena el despliegue hasta que el dueño active la protección de main (acciones pendientes).
  2. La web y la API se despliegan por separado. Puedes revertir una sin tocar la otra (runbook 9.9).
  3. Las variables NEXT_PUBLIC_* se fijan al compilar la web. Cambiarlas en Vercel no tiene efecto hasta que vuelvas a desplegar.

2. Dónde corre cada pieza

Pieza Dónde corre Cómo se despliega Configuración en el repositorio
Web (Next.js 14) Vercel, proyecto koptup (tiene los dominios www.koptup.com y koptup.com), región iad1 Integración de Vercel con GitHub: cada push a main va a producción; las demás ramas crean una vista previa apps/web/vercel.json (npm install, next build) y apps/web/.env.production (solo URL públicas)
API (Express) Railway, un servicio con apps/backend Integración de Railway con GitHub: build con Nixpacks (npm install && npm run build), arranque con npm run start (node dist/index.js) apps/backend/railway.json (una instancia, chequeo GET /health —200 solo con MongoDB conectado—, 120 s, reinicio si falla hasta 10 veces) y apps/backend/Procfile
Base de datos MongoDB (la cadena de conexión está en Railway, nunca en el repositorio) Fuera del repositorio —
Caché, cupos y gasto de IA Redis (en Railway) Fuera del repositorio —
IA OpenAI, modelo gpt-4o-mini por defecto — —
Correo y WhatsApp Resend (API HTTPS, dominio koptup.com) y WhatsApp Cloud API de Meta con la plantilla koptup_aviso. SMTP solo como respaldo (Railway Free, Trial y Hobby lo bloquean) Se configuran en sus paneles y con variables en Railway (4.5) apps/backend/.env.example (solo nombres)
CI, monitoreo y wiki GitHub Actions Automático .github/workflows/ci.yml, .github/workflows/monitor.yml y .github/workflows/wiki-sync.yml

Archivos subidos en Railway: los adjuntos de pedidos y los archivos de cuentas médicas se guardan en uploads/ (en Railway, /app/uploads). Sin un volumen montado en esa ruta se borran en cada despliegue (acciones pendientes). Los bots del chatbot y sus conversaciones viven en MongoDB, y los registros (logs/) se consultan en la plataforma.

koptup.com (sin www) responde con una redirección permanente a https://www.koptup.com/. Las vistas previas de Vercel (ramas distintas de main) están protegidas con el inicio de sesión de Vercel; los dominios propios no.

La arquitectura completa, con lo que hace cada pieza, está en Arquitectura.


3. Integración continua (CI)

El workflow CI (.github/workflows/ci.yml) corre en cada pull request, en cada push a main y a mano (workflow_dispatch), con Node 20. Si haces push otra vez a la misma rama, la corrida anterior se cancela (concurrency).

flowchart TD
    classDef sistema fill:#e0e7ff,stroke:#4f46e5,color:#312e81
    classDef externo fill:#f1f5f9,stroke:#64748b,color:#334155
    classDef admin fill:#fef3c7,stroke:#d97706,color:#78350f
    classDef error fill:#fee2e2,stroke:#dc2626,color:#7f1d1d

    T["Disparo: pull request, push a main o manual"]:::admin
    subgraph J1["Job calidad: Lint y tipos"]
        Q1["npm ci"]:::externo
        Q2["Mensajes i18n agregados al día"]:::sistema
        Q3["ESLint de web y backend"]:::sistema
        Q4["TypeScript: tsc de web y backend"]:::sistema
        Q5["Títulos de página: check-titles"]:::sistema
        Q1 --> Q2 --> Q3 --> Q4 --> Q5
    end
    subgraph J2["Job pruebas: matriz backend y web"]
        P0["Servicios: MongoDB 7 y Redis 7"]:::externo
        P1["Jest del backend: unitarias e integración"]:::sistema
        P2["Jest de la web: unitarias y humo de demos"]:::sistema
        P0 --> P1
        P0 --> P2
    end
    subgraph J3["Job build"]
        B1["tsc del backend"]:::sistema
        B2["next build de la web, apuntando a localhost:3001"]:::sistema
        B3["Guarda dist y .next como artefacto"]:::externo
        B1 --> B2 --> B3
    end
    subgraph J4["Job e2e: Playwright"]
        E1["MongoDB 7, Redis 7, mock de OpenAI<br/>y mock de Resend y WhatsApp"]:::externo
        E2["Backend compilado en modo producción"]:::sistema
        E3["Web compilada con next start"]:::sistema
        E4["Suite de Playwright: escritorio, móvil y catálogo"]:::sistema
        E1 --> E2 --> E3 --> E4
    end
    subgraph J5["Job seguridad"]
        S1["npm audit de dependencias de producción"]:::sistema
        S2["gitleaks: archivos y commits nuevos"]:::sistema
        S1 --> S2
    end
    R{"¿Todo en verde?"}
    OK["Listo para revisar y fusionar"]:::sistema
    KO["Revisa los logs y el reporte de Playwright"]:::error

    T --> J1
    T --> J2
    T --> J3
    T --> J5
    B3 -->|"needs: build"| J4
    J1 --> R
    J2 --> R
    J4 --> R
    J5 --> R
    R -->|"Sí"| OK
    R -->|"No"| KO
Loading

Cinco jobs: cuatro empiezan a la vez y el de punta a punta espera los compilados del build.

Job Qué verifica Falla si…
Lint y tipos Que apps/web/messages/_*.json estén regenerados, ESLint, tsc --noEmit (incluidas las pruebas) y el formato de los títulos Alguien cambió textos sin correr node apps/web/scripts/merge-messages.mjs, hay un error de tipos o un título pasa de 60 caracteres
Pruebas unitarias (backend) Jest con MongoDB y Redis reales: corren también las pruebas de integración (permisos por rol, sistema de demos, chatbot, topes de gasto, perfil) Una regla de negocio o de autorización cambió
Pruebas unitarias (web) Jest con jsdom: cada demo se renderiza con los textos reales en español y se prueba el middleware de acceso Una página lanza un error, no pinta nada o usa una clave de traducción que no existe
Build tsc del backend y next build de la web El código no compila
E2E (Playwright) Solicitud → aprobación → activación → acceso → revocación, permisos, cambio de modo desde el panel, contacto con sus avisos al dueño (contra el mock de Resend y WhatsApp: nunca sale un aviso real), «Prueba con tu documento» con un PDF, humo de todas las páginas y demos Un recorrido real se rompió
Seguridad npm audit de las dependencias de producción con umbral alto (scripts/audit-check.mjs) y gitleaks sobre los archivos y los commits nuevos del push o del PR (.gitleaks.toml) Aparece una alerta alta o crítica que nadie revisó, vence la fecha de revisión de .github/audit-allowlist.json o un commit trae algo con forma de secreto

Tiempos reales de la última corrida sobre main (8 de octubre de 2026, hora UTC; 9 minutos en total):

gantt
    title CI sobre main, 8 de octubre de 2026 (UTC)
    dateFormat HH:mm:ss
    axisFormat %H:%M
    section Calidad
    Lint y tipos           :a1, 22:21:07, 22:24:03
    section Pruebas
    Jest backend           :a2, 22:21:07, 22:23:59
    Jest web               :a3, 22:21:07, 22:23:35
    section Build y e2e
    Build                  :a4, 22:21:09, 22:25:05
    E2E Playwright         :a5, 22:25:07, 22:30:04
Loading

La suite de punta a punta es la parte más larga (unos 5 minutos) porque espera al build.

Los valores de entorno que usa CI (por ejemplo, los dos JWT de prueba) son ficticios y viven en el propio workflow: CI no usa ningún secreto de GitHub.

Job de seguridad. Las alertas altas o críticas de dependencias que ya se revisaron y solo se corrigen con un cambio de versión mayor están en .github/audit-allowlist.json, con el motivo y una fecha límite de revisión (revisarAntesDe): el job las muestra como aviso en cada corrida y falla con cualquier alerta nueva o cuando la fecha vence. gitleaks solo revisa los commits nuevos (los del PR o los del push), no toda la historia. Para correrlo igual en tu computador: node scripts/audit-check.mjs y, con el binario de gitleaks instalado, gitleaks dir . --config .gitleaks.toml en una copia sin node_modules. El paso a paso con diagrama del ciclo PR → merge está en Flujos de administración y operación › 7.


4. Variables de entorno

4.1 Cómo se leen

  • Web (Vercel): Project › Settings › Environment Variables del proyecto koptup. Las NEXT_PUBLIC_* se copian dentro del código al compilar: después de cambiarlas hay que redesplegar. Las demás (solo servidor) también se leen en cada despliegue.
  • Backend (Railway): pestaña Variables del servicio. Se leen al arrancar, así que cada cambio se aplica con un despliegue nuevo (Railway te pide confirmarlo).
  • Al arrancar, el backend revisa sus variables con apps/backend/src/config/env.ts: en producción no abre el puerto si falta MONGODB_URI o JWT_SECRET y escribe Configuración incompleta: … en el log. Para el resto solo deja un aviso [env] NOMBRE: … (nunca el valor) y la función afectada se apaga o usa su valor por defecto. El diagrama de esa revisión está en Flujos técnicos › 7.1.
  • Ningún secreto va en el repositorio. Las plantillas apps/backend/.env.example y apps/web/.env.example solo tienen nombres y valores de ejemplo.

Lo que se apaga si falta cada variable importante:

flowchart LR
    classDef sistema fill:#e0e7ff,stroke:#4f46e5,color:#312e81
    classDef error fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
    classDef admin fill:#fef3c7,stroke:#d97706,color:#78350f

    M["MONGODB_URI o JWT_SECRET"]:::admin --> M1["La API no arranca en producción"]:::error
    JR["JWT_REFRESH_SECRET"]:::admin --> J1["Arranca, pero nadie puede iniciar sesión"]:::error
    R["REDIS_URL"]:::admin --> R1["IA pública apagada y la sesión no se renueva (dura 15 min)"]:::error
    O["OPENAI_API_KEY"]:::admin --> O1["Chatbot en modo extractivo; demos con IA apagadas"]:::error
    D["DEMO_UPLOAD_ENABLED distinta de true"]:::admin --> D1["Prueba con tu documento apagada"]:::error
    S["RESEND_API_KEY (o el respaldo SMTP)"]:::admin --> S1["No sale ningún correo; el panel muestra el enlace para copiar"]:::sistema
    NE["NOTIFY_EMAIL y ADMIN_EMAIL"]:::admin --> NE1["Los avisos de contactos no llegan por correo"]:::error
    WA["WHATSAPP_API_TOKEN, WHATSAPP_PHONE_NUMBER_ID<br/>o ADMIN_WHATSAPP_NUMBER"]:::admin --> WA1["Los avisos de contactos no llegan por WhatsApp"]:::error
    F["FRONTEND_URL"]:::admin --> F1["Los enlaces de los correos apuntan a localhost"]:::error
    A["NEXT_PUBLIC_API_URL en Vercel"]:::admin --> A1["La web usa la URL de .env.production"]:::sistema
    G["NEXT_PUBLIC_GA_ID y similares"]:::admin --> G1["Sin banner de cookies y sin medición"]:::sistema
Loading

En rojo, lo que el visitante o el dueño notan; en morado, lo que tiene una alternativa.

4.2 Web (Vercel)

Variable ¿Obligatoria? Por defecto Qué hace
NEXT_PUBLIC_API_URL Sí en la práctica En Vercel y en next build/next start: la de apps/web/.env.production (la URL pública de la API en Railway). En next dev y en las pruebas: http://localhost:3001 (src/lib/backend-url.ts), para que un entorno de desarrollo nunca llame a producción URL del backend. La usan el navegador, el middleware que protege /admin, /dashboard y las demos con acceso, y la CSP (connect-src). Cambiarla exige redesplegar
INTERNAL_API_KEY Opcional (solo servidor, nunca NEXT_PUBLIC_) — Clave compartida con el backend: el middleware y el proxy de LinkedIn Ads informan la IP real del visitante. Debe ser igual en Vercel y en Railway
NEXT_PUBLIC_GA_ID Opcional — ID de Google Analytics 4 (G-…). Ver SEO, analítica y legal
NEXT_PUBLIC_GOOGLE_ADS_ID Opcional — Etiqueta de Google Ads (AW-… o solo el número)
NEXT_PUBLIC_LINKEDIN_PARTNER_ID Opcional — Partner ID numérico de LinkedIn Insight Tag
_next_intl_trailing_slash Interna false La define next.config.js para next-intl. No la toques

El dominio canónico del sitio está fijo en src/lib/site.ts: no hay variable para cambiarlo. Si el proyecto de Vercel todavía tiene NEXT_PUBLIC_SITE_URL, se puede borrar (el código no la lee).

4.3 Backend (Railway)

Imprescindibles

Variable ¿Obligatoria? Por defecto Qué hace
NODE_ENV Sí: production development Activa las reglas de producción: revisa las imprescindibles, limita CORS a koptup.com, www.koptup.com y CORS_ORIGIN, esconde /api-docs e ignora DEMO_RAG_TTL_SECONDS
MONGODB_URI Sí (sin ella no arranca) — Conexión a MongoDB (mongodb:// o mongodb+srv://). Si la base no responde, la API arranca igual y GET /health da 503
JWT_SECRET Sí (sin ella no arranca) — Firma los tokens de acceso (15 min). Largo (32 o más caracteres; si es más corto deja un aviso) y aleatorio
JWT_REFRESH_SECRET Sí para iniciar sesión — Firma los tokens de renovación (7 días). Distinta de JWT_SECRET. Si falta, la API arranca con un aviso y el inicio de sesión falla

Servidor y red

Variable ¿Obligatoria? Por defecto Qué hace
PORT La pone Railway 3001 Puerto de la API
FRONTEND_URL Recomendada: https://www.koptup.com http://localhost:3000 Base de los enlaces que arma el backend: activación (/activar/<token>), Mis demos, restablecer contraseña, correos y regreso del login con Google. El generador de LinkedIn usa https://www.koptup.com si falta
CORS_ORIGIN Opcional — Orígenes extra, separados por coma. En producción siempre se aceptan https://koptup.com y https://www.koptup.com
TRUST_PROXY_HOPS Recomendada: 1 1 Proxies delante de la API (Railway = 1). Define la IP real del visitante para los cupos por IP
INTERNAL_API_KEY Recomendada (igual que en Vercel) — Acepta la IP real que informa la web en X-Client-IP. Sin ella, esas peticiones cuentan con la IP de salida de Vercel, y el arranque lo avisa
API_URL Opcional http://localhost:3001 URL que muestra /api-docs y base del enlace de descarga de facturas
API_DOCS_ENABLED Opcional false /api-docs solo existe fuera de producción o con true. No la actives en el entorno público

Sesión, cupos y Redis

Variable ¿Obligatoria? Por defecto Qué hace
REDIS_URL Recomendada redis://localhost:6379 Tokens de renovación, cupos compartidos, gasto mensual de IA y candados de los jobs (vencimiento, conservación y avisos)
JWT_EXPIRES_IN Opcional 15m Vida del token de acceso
JWT_REFRESH_EXPIRES_IN Opcional 7d Vida de la sesión renovable (7d, 12h, 3600…). La misma cifra firma el token y vence su copia en Redis. Máximo 7 días, lo que dura la cookie de la web: un valor mayor o inválido usa 7 días y deja un aviso al arrancar
RATE_LIMIT_WINDOW_MS Opcional 900000 (15 min) Ventana del límite general de la API
RATE_LIMIT_MAX_REQUESTS Opcional 100 Peticiones por ventana, por cuenta (con sesión) o por IP
AUTH_ME_RATE_LIMIT_MAX Opcional 120 por minuto Cupo de GET /api/auth/me y GET /api/demo-access/<slug>, que la web consulta en cada navegación protegida
CHATBOT_RATE_LIMIT_MAX Opcional 60 por minuto Preguntas por IP al chat de los bots (POST /api/chatbot/bots/:id/chat)
CHATBOT_API_RATE_LIMIT_MAX Opcional 180 por minuto Resto de peticiones por IP a /api/chatbot (GET /models no cuenta)
CHATBOT_CREATE_LIMIT_PER_HOUR Opcional 30 Bots nuevos por IP cada hora en «Configura el tuyo»
LOGIN_RATE_LIMIT_MAX, LOGIN_IP_RATE_LIMIT_MAX Opcional 10 y 30 cada 15 min Intentos de inicio de sesión por IP y correo, y por IP
REGISTER_RATE_LIMIT_MAX Opcional 5 por hora Registros por IP
FORGOT_PASSWORD_RATE_LIMIT_MAX Opcional 3 cada 15 min Enlaces de recuperación por IP y correo (además, 10 por IP con correos distintos)
RESET_PASSWORD_RATE_LIMIT_MAX, ACTIVATION_RATE_LIMIT_MAX Opcional 10 cada 15 min Intentos de restablecer la contraseña y de activar la cuenta, por IP
CHANGE_PASSWORD_RATE_LIMIT_MAX Opcional 5 cada 15 min Cambios de contraseña por cuenta

IA y topes de gasto (mes UTC, acumulado en Redis con los tokens que informa OpenAI)

Variable ¿Obligatoria? Por defecto Qué hace
OPENAI_API_KEY Recomendada — Clave de OpenAI. Sin ella el chatbot responde en modo extractivo y las demás funciones con IA se apagan
OPENAI_MODEL Opcional gpt-4o-mini (gestor de contenido) y gpt-4o (módulos de cuentas médicas) Modelo de esas dos funciones. El chatbot, «Prueba con tu documento» y LinkedIn no la usan
ANTHROPIC_API_KEY Opcional — Interpretación con IA de reglas de facturación en la demo privada de cuentas médicas
CHATBOT_MONTHLY_BUDGET_USD Opcional 50 Tope del chatbot (también cubre los asistentes de las demos de mesa de ayuda, HRMS y LMS, que usan /api/chatbot). Al llegar, responde en modo extractivo
DEMO_MONTHLY_BUDGET_USD Opcional 50 Tope de «Prueba con tu documento». Al llegar, se apaga hasta el mes siguiente
LINKEDIN_ADS_MONTHLY_BUDGET_USD Opcional 20 Tope del generador de LinkedIn Ads
CONTENT_MONTHLY_BUDGET_USD Opcional 20 Tope del asistente del gestor de contenido
DOCUMENTS_MONTHLY_BUDGET_USD Opcional 10 Tope del gestor de documentos (/api/documents). Al llegar, una subida se guarda sin análisis y las demás funciones con IA responden «no disponible»
HEALTH_MONTHLY_BUDGET_USD Opcional 30 Tope de las APIs de salud (cuentas médicas, auditoría, sistema experto, CUPS y reglas de facturación)
DOCUMENTS_AI_DAILY_LIMIT_PER_USER, HEALTH_AI_DAILY_LIMIT_PER_USER Opcional 30 y 20 Solicitudes con IA por cuenta y por día en esas dos funciones. El equipo de KopTup solo tiene el tope mensual

Un tope con un valor inválido o negativo deja esa función en 0, es decir, apagada.

«Prueba con tu documento»

Variable ¿Obligatoria? Por defecto Qué hace
DEMO_UPLOAD_ENABLED Para encenderla: true false Encendido explícito. Además necesita Redis conectado y OPENAI_API_KEY (runbook 9.3). Si falta, el arranque avisa que la función quedó apagada (con false explícito no avisa)
DEMO_RAG_TTL_SECONDS No la definas en Railway — Solo pruebas locales: acorta la hora de vida de los documentos. Se ignora con NODE_ENV=production

Sistema de demos y datos personales

Variable ¿Obligatoria? Por defecto Qué hace
PRIVACY_POLICY_VERSION Opcional 2026-10 Versión de la política que se guarda con cada autorización (Ley 1581). Cámbiala cuando publiques una política nueva
DEMO_REQUEST_LIMIT_PER_HOUR Opcional 5 Solicitudes válidas por IP cada hora en /solicitar-demo
DEMO_REQUEST_LIMIT_PER_EMAIL_DAY Opcional 3 Solicitudes válidas por email al día
DEMO_GRANTS_JOB_ENABLED Opcional true Job que marca los accesos vencidos y envía el recordatorio 3 días antes. false lo apaga en esa instancia
DEMO_GRANTS_JOB_INTERVAL_MS Opcional 3600000 (1 hora; mínimo 60000) Cada cuánto corre el job
DATA_RETENTION_MONTHS Opcional 24 (1 a 120) Plazo de conservación: a los cuántos meses se anonimizan las solicitudes de demo rechazadas y los prospectos sin actividad (sin cuenta ni contrato). Valor conservador: valídalo con tu asesor legal
RETENTION_JOB_ENABLED Opcional true Job diario de conservación (anonimiza lo vencido y borra los bots del chatbot sin uso), con candado en Redis. false lo apaga en esa instancia
RETENTION_JOB_INTERVAL_MS Opcional 86400000 (1 día; mínimo 60000) Cada cuánto corre el job de conservación

Flujo de compra (en desarrollo)

Variable ¿Obligatoria? Por defecto Qué hace
COMMERCE_ENABLED No la definas en Railway (o déjala en false) false Enciende el flujo de compra, que todavía no está terminado ni revisado: pipeline de leads, propuestas, portal comercial y pagos. Apagado (lo normal): sus rutas (/api/admin/leads, /api/admin/proposals, /api/admin/settings, /api/proposals/public, las de propuestas en /api/me y /api/payments) responden 404, el job de vencimiento de propuestas no corre, el arranque no liga los contactos a leads y ningún formulario ni acceso a demos escribe leads. Las variables PROPOSALS_JOB_* y WOMPI_* solo tienen efecto con este interruptor en true

Correo y WhatsApp: avisos al dueño y correos a los prospectos

Cada contacto nuevo (formulario de /contact, «Solicitar demo» y «Prueba con tu documento») avisa al dueño por correo y por WhatsApp; el resultado de cada canal queda en el contacto. Railway Free, Trial y Hobby bloquean el SMTP saliente (puertos 25, 465 y 587): por eso el correo sale por la API HTTPS de Resend. El paso a paso para configurarlo todo está en la sección 4.5.

Variable ¿Obligatoria? Por defecto Qué hace
RESEND_API_KEY Necesaria para enviar correo en Railway — Key de Resend con permiso «Sending access». Con ella todos los correos salen por Resend: avisos al dueño, acuse y aprobación de demos, recordatorios, restablecer contraseña y avisos de propuestas. Sin ella ni SMTP no sale ningún correo y el arranque lo avisa
EMAIL_FROM Opcional KopTup <avisos@koptup.com> Remitente. Su dominio debe estar verificado en Resend (no hace falta un buzón). Si Resend responde que el dominio no está verificado, el aviso al dueño se reintenta una vez desde onboarding@resend.dev, que solo entrega al correo dueño de la cuenta de Resend
NOTIFY_EMAIL Recomendada ADMIN_EMAIL A quién le llegan los avisos por correo (uno o varios, separados por coma). El aviso de un contacto lleva «Responder a» con el email de la persona, así que respondes directo desde tu correo
ADMIN_EMAIL Recomendada — Destino de los avisos por correo solo si falta NOTIFY_EMAIL. Además, el arranque le da el rol admin a esa cuenta solo si existe, está activa y no hay ningún otro admin; el cambio queda en la bitácora (runbook 9.4)
SMTP_HOST, SMTP_PORT, SMTP_SECURE, SMTP_USER, SMTP_PASS Opcionales smtp.gmail.com, 587, false Respaldo: solo se usan sin RESEND_API_KEY (desarrollo local o Railway Pro). Con SMTP y sin Resend, el arranque avisa que Railway Free, Trial y Hobby lo bloquean. Esperan 10 s para conectar y 15 s para enviar
WHATSAPP_PROVIDER Opcional meta si hay token o id del número Único valor soportado: meta (acepta el viejo whatsapp-business). Twilio y UltraMsg se retiraron: con twilio o ultramsg el canal queda apagado y el arranque lo avisa
WHATSAPP_API_TOKEN Para avisos por WhatsApp — Token permanente de un usuario del sistema de Meta (caducidad «Nunca», permisos whatsapp_business_messaging y whatsapp_business_management). El token temporal del panel de desarrolladores vence en horas
WHATSAPP_PHONE_NUMBER_ID Para avisos por WhatsApp — «Identificador del número de teléfono» que envía (WhatsApp › Configuración de la API). Es un id numérico, no el teléfono
ADMIN_WHATSAPP_NUMBER Para avisos por WhatsApp — Número que recibe los avisos, con indicativo (57 y los 10 dígitos; se toman solo los dígitos). Con el número de prueba de Meta debe estar en su lista de destinatarios verificados
WHATSAPP_TEMPLATE_NAME, WHATSAPP_TEMPLATE_LANG Opcionales koptup_aviso, es Plantilla aprobada que se envía. Deben ser iguales a las de Meta
WHATSAPP_GRAPH_VERSION Opcional v25.0 Versión de la Graph API. Meta retira cada versión unos 2 años después de publicarla: súbela antes de que venza
WHATSAPP_HELLO_WORLD_FALLBACK Opcional true Si la plantilla propia falla por un error de plantilla (códigos 132000, 132001, 132005, 132007, 132012, 132015, 132016 o 132018), envía la plantilla de ejemplo hello_world para que al menos suene el teléfono. El panel lo muestra como «respaldo». false lo apaga
WHATSAPP_APP_SECRET, WHATSAPP_WEBHOOK_VERIFY_TOKEN Opcionales (las dos juntas) — Encienden el webhook firmado /api/webhooks/whatsapp: Meta confirma entregado, leído o fallido y, si un aviso falla, te llega un correo de alerta. Sin las dos la ruta responde 404 y el panel solo sabe que Meta aceptó el mensaje
NOTIFY_HEARTBEAT_DAYS Opcional 30 Prueba mensual («latido»): cada N días, entre las 9:00 y las 18:59 hora de Bogotá, un aviso de prueba por cada canal configurado; si uno falla (o solo sale por un respaldo), avisa por el otro, y si no sale por ninguno lo reintenta al día siguiente. 0 la apaga
NOTIFY_JOBS_ENABLED Opcional true false apaga en esa instancia los reintentos (cada 5 min) y la prueba mensual. Los avisos de cada contacto nuevo siguen saliendo

Chatbot, archivos e inicio de sesión con Google

Variable ¿Obligatoria? Por defecto Qué hace
CHATBOT_EXAMPLE_BOT_IDS Opcional — IDs (separados por coma) de los bots que todos ven en «Configura el tuyo»
CHATBOT_CONVERSATION_TTL_DAYS Opcional 30 (1 a 365) Días que se guarda cada pregunta al widget de un bot de usuario (las de «Prueba el asistente», la vista previa y los bots de ejemplo no se guardan)
CHATBOT_SAMPLE_BOT_RETENTION_DAYS, CHATBOT_BOT_RETENTION_DAYS Opcionales 30 y 365 Días sin uso tras los que el job de conservación borra un bot de ejemplo o de «Configura el tuyo»
CHATBOT_MAX_BOTS Opcional 20000 Bots que puede haber a la vez en la base
CHATBOT_STATE_DIR Opcional ./data/chatbots Carpeta del archivo heredado state.json: si existe al arrancar, se migra a MongoDB una vez y se borra. Los bots ya viven en MongoDB y sobreviven a los despliegues
MAX_FILE_SIZE Opcional 10485760 (10 MB) Tamaño máximo de archivo en las rutas de carga generales
ALLOWED_FILE_TYPES Opcional pdf,docx,txt,csv Extensiones aceptadas en esas rutas
CUPS_IMPORT_DIR Opcional ./data/imports Única carpeta desde la que el admin importa códigos CUPS
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_S3_BUCKET Opcionales (las tres juntas) — Si existen, los documentos del portal se suben a S3; si no, quedan en el disco del servidor
AWS_REGION Opcional us-east-1 Región del bucket
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET Opcionales (las dos juntas) — Encienden «Continuar con Google»
GOOGLE_CALLBACK_URL Con Google http://localhost:3001/api/auth/google/callback URL de regreso registrada en Google

4.4 Solo para pruebas y desarrollo (nunca en producción)

Variable Dónde Para qué
OPENAI_BASE_URL Backend Apunta la API al mock de OpenAI (http://127.0.0.1:3999/v1)
SEED_ADMIN_PASSWORD, SEED_CLIENT_PASSWORD Scripts de semilla del backend Contraseñas de las cuentas de ejemplo; si faltan, se genera una al azar
MONGODB_URI_TEST, REDIS_URL_TEST Jest del backend Base y Redis de pruebas; sin ellas se omiten las pruebas de integración
E2E_BASE_URL, E2E_API_URL Playwright Web (por defecto http://localhost:3300) y backend contra los que corre la suite
E2E_MONGODB_URI o E2E_ADMIN_EMAIL + E2E_ADMIN_PASSWORD Playwright Cómo obtener la cuenta admin de pruebas
E2E_ALLOW_REMOTE, E2E_INCLUDE_KNOWN_ISSUES Playwright Permitir un entorno no local desechable; exigir cero errores también en demos con problemas conocidos
OPENAI_MOCK_PORT, OPENAI_MOCK_HOST, OPENAI_MOCK_LOG Mock de OpenAI Puerto (3999), host (127.0.0.1) y archivo de registro
RESEND_API_URL, WHATSAPP_GRAPH_URL Backend Apuntan Resend y la Graph API de Meta al mock de avisos (http://127.0.0.1:3998), para que ninguna prueba envíe un correo o un WhatsApp real
E2E_NOTIFY_MOCK_URL, E2E_NOTIFY_EMAIL, E2E_ADMIN_WHATSAPP_NUMBER Playwright Dónde está el mock de avisos y los destinos ficticios que tiene el backend; sin la primera, owner-notify.spec.ts se omite
NOTIFY_MOCK_PORT, NOTIFY_MOCK_HOST, NOTIFY_MOCK_LOG Mock de Resend y WhatsApp Puerto (3998), host (127.0.0.1) y archivo de registro

4.5 Configurar los avisos al dueño, paso a paso

Qué consigues: cada vez que alguien escribe desde el sitio (formulario de /contact, «Solicitar demo» o «Prueba con tu documento») te llega un correo a tu Gmail y un WhatsApp a tu celular, aunque pasen meses sin mensajes. Toma unos 45 minutos de trabajo; lo más largo es esperar que Meta apruebe la plantilla (de minutos a 24 horas). Los valores reales (keys, tokens, tu número completo) van solo en Railway: nunca en esta wiki ni en el repositorio, que son públicos.

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

    V["Persona: /contact, Solicitar demo<br/>o Prueba con tu documento"]:::visitante
    API["Backend en Railway<br/>guarda el contacto"]:::sistema
    RS["Resend, API HTTPS<br/>desde avisos@koptup.com"]:::externo
    ME["Meta, WhatsApp Cloud API<br/>plantilla koptup_aviso"]:::externo
    GM["Tu Gmail<br/>filtro Nunca enviar a spam"]:::admin
    WA["Tu WhatsApp<br/>+57 3XX XXX XXXX"]:::admin
    PN["Admin › Contactos<br/>estado del aviso por canal"]:::sistema
    JOB["Job cada 5 min<br/>reintenta lo que falló"]:::sistema
    LAT["Prueba mensual<br/>por los dos canales"]:::sistema

    V --> API
    API -->|"correo con Responder a la persona"| RS --> GM
    API -->|"plantilla con título y detalle"| ME --> WA
    ME -.->|"webhook opcional: entregado, leído o fallido"| API
    API --> PN
    JOB -.-> API
    LAT -.-> API
Loading

Los dos canales son independientes: si uno falla, el otro sale igual. El resultado de cada uno queda en el contacto.

Paso 1. Resend: la key para enviar correo

koptup.com ya está verificado en Resend (región São Paulo): en Namecheap existen sus registros resend._domainkey (TXT) y send (MX y TXT con SPF). No los borres.

  1. Entra a resend.com con la cuenta que tiene koptup.com. En Domains debe decir Verified.
    • Si no encuentras esa cuenta: un dominio solo puede estar activo en una cuenta de Resend a la vez. Agrégalo en tu cuenta y sigue la opción de Resend para reclamarlo (te pide un registro TXT nuevo en Namecheap; el reclamo vence a los 7 días).
  2. API Keys › Create API Key: nombre koptup-railway, permiso Sending access, dominio koptup.com. Copia la key: empieza por re_ y se muestra una sola vez.
  3. El plan Free alcanza de sobra: 3.000 correos al mes y 100 al día, y la key no vence.

Paso 2. Meta: app, número de prueba gratis y tu número como destinatario

  1. En developers.facebook.com › Mis apps › Crear app, elige el caso de uso de WhatsApp (conectar con clientes por WhatsApp) y el portafolio comercial de KopTup (o crea uno).
  2. En la app, abre WhatsApp › Configuración de la API (API Setup). Meta te da un número de prueba gratis. Copia el Identificador del número de teléfono (Phone number ID): va en WHATSAPP_PHONE_NUMBER_ID.
  3. En esa misma pantalla, en Para (To) › Administrar la lista de números de teléfono, agrega tu celular (+57 3XX XXX XXXX) y escribe el código que te llega por WhatsApp. Con el número de prueba solo se puede escribir a números verificados en esa lista (máximo 5).
  4. Opcional: pulsa Enviar mensaje con la plantilla hello_world. Si te llega, el número y el destinatario están bien.

El número de prueba no pide tarjeta, SIM ni verificar el negocio, y para enviar los avisos la app puede quedarse en modo de desarrollo (solo el webhook del paso 6 pide pasarla a modo Live). Si algún día usas un número propio, Meta cobra cada plantilla de utilidad entregada (a Colombia, unos USD 0,0008 por mensaje en la tarifa vigente desde octubre de 2026) y exige un método de pago.

Paso 3. La plantilla koptup_aviso

Fuera de una conversación abierta en las últimas 24 horas, Meta solo entrega plantillas aprobadas. Con un aviso al mes esa ventana casi nunca está abierta, así que el backend siempre usa esta plantilla.

  1. Abre el Administrador de WhatsApp (WhatsApp Manager, en business.facebook.com) en la cuenta de WhatsApp del número de prueba › Plantillas de mensajes › Crear plantilla.

  2. Categoría Utility (Utilidad), nombre koptup_aviso, idioma Spanish (es).

  3. Cuerpo, copiado exacto (2 variables de tipo número, sin encabezado, pie ni botones):

    Aviso de KopTup: {{1}}. Detalle: {{2}}. Revisa tu correo o el panel de KopTup para ver todo y responder.
    
  4. Ejemplos que pide la revisión: {{1}} = Nuevo contacto de Ana Pérez; {{2}} = Empresa: Ejemplo SAS · Servicio: Sistema RAG · Email: ana@ejemplo.co · Mensaje: Quiero una demo.

  5. Enviar a revisión. Espera a que quede Activa (aprobada). Si Meta la rechaza o la pasa a otra categoría, revisa que el texto sea exactamente el de arriba.

No cambies el texto ni la cantidad de variables: el backend envía siempre 2 ({{1}} título de hasta 120 caracteres; {{2}} detalle de hasta 700, con empresa, servicio, email y un extracto del mensaje, sin saltos de línea). Si la plantilla falla, el backend envía la de ejemplo hello_world (ya existe en la cuenta de prueba) para que al menos suene el teléfono, y el panel te dice que la plantilla necesita arreglo.

Paso 4. Un token de Meta que no vence

El token que muestra Configuración de la API es temporal y vence en horas. Para producción se usa el de un usuario del sistema:

  1. En business.facebook.com › Configuración (del negocio) › Usuarios › Usuarios del sistema › Agregar: nombre koptup-avisos, rol Administrador.
  2. Asignar activos: la app del paso 2 y la cuenta de WhatsApp del número, las dos con control total.
  3. Generar nuevo token: elige la app, caducidad Nunca y los permisos whatsapp_business_messaging y whatsapp_business_management. Copia el token (se muestra una sola vez): va en WHATSAPP_API_TOKEN.

Paso 5. Las variables en Railway

En Railway › servicio del backend › Variables:

Variable Valor
RESEND_API_KEY La key del paso 1
EMAIL_FROM KopTup <avisos@koptup.com> (o no la definas: es el valor por defecto)
NOTIFY_EMAIL Tu Gmail (si quieres, varios separados por coma)
WHATSAPP_PROVIDER meta
WHATSAPP_API_TOKEN El token del paso 4
WHATSAPP_PHONE_NUMBER_ID El id del paso 2
ADMIN_WHATSAPP_NUMBER Tu celular con indicativo: 57 seguido de los 10 dígitos (573XXXXXXXXX)

Las demás (WHATSAPP_TEMPLATE_NAME, WHATSAPP_TEMPLATE_LANG, WHATSAPP_GRAPH_VERSION, NOTIFY_HEARTBEAT_DAYS…) ya tienen el valor correcto por defecto (4.3). Borra cualquier TWILIO_*, ULTRAMSG_* o WHATSAPP_PROVIDER=twilio que quede de antes, y no definas RESEND_API_URL ni WHATSAPP_GRAPH_URL (son solo para pruebas). Guarda y despliega.

Después, en Settings del mismo servicio, deja apagado «Serverless» (antes App Sleeping). Con el servicio dormido un formulario igual lo despierta, pero mientras duerme no corren los reintentos ni la prueba mensual, que son las que evitan que un canal se caiga sin que lo notes.

En el registro del despliegue deben aparecer estas dos líneas (los destinos salen enmascarados):

Avisos por correo: Resend → tu***@gmail.com
Avisos por WhatsApp: Meta plantilla koptup_aviso → +57 *** *** XXXX

Si alguna dice no configurado: falta …, define esa variable. Los avisos [env] … del arranque también dicen qué falta y qué se apaga.

Paso 6. Webhook de WhatsApp (opcional, recomendado)

Sin webhook, el panel solo sabe que Meta aceptó el mensaje. Con él sabe si se entregó, se leyó o falló, y si falla te llega un correo de alerta con el código de Meta y qué revisar.

  1. Pasa la app a modo Live: en el panel de la app de Meta, Publicar (o el interruptor Modo de la app: Desarrollo → En vivo). Meta pide la URL de la política de privacidad: usa https://www.koptup.com/privacy. La guía oficial de webhooks de Meta lo advierte: en modo de desarrollo algunos webhooks no se envían, así que los estados (entregado, leído, fallido) podrían no llegar nunca.
  2. Inventa un texto largo al azar para la verificación, por ejemplo con node -e "console.log(require('crypto').randomBytes(24).toString('hex'))": va en WHATSAPP_WEBHOOK_VERIFY_TOKEN.
  3. En la app de Meta › Configuración de la app › Básica › Clave secreta de la app (Mostrar): va en WHATSAPP_APP_SECRET.
  4. Define las dos en Railway y despliega antes del paso siguiente: sin ellas la ruta responde 404 y Meta no la puede verificar.
  5. En la app › WhatsApp › Configuración › Webhook › Editar: URL de devolución de llamada https://<dominio de la API>/api/webhooks/whatsapp y, como token de verificación, el mismo texto del punto 1. Verificar y guardar.
  6. En Campos del webhook, suscribe messages (por ahí llegan los estados de los mensajes enviados).

El backend rechaza con 401 todo evento que no venga firmado con la clave secreta de la app (cabecera X-Hub-Signature-256) y acepta lotes de hasta 3 MB (el máximo que documenta Meta). En el panel, «Confirmación de entrega» dice Configurada, sin eventos aún hasta que llegue el primer estado de Meta, y desde ahí muestra la fecha del último evento recibido. Si después de Enviar aviso de prueba sigue sin eventos, revisa que la app esté en modo Live (punto 0) y que el campo messages esté suscrito.

Paso 7. Namecheap: reenvío de ronald@koptup.com y DMARC

ronald@koptup.com aparece en el sitio, pero hoy koptup.com no recibe correo (la raíz no tiene registro MX): lo que te escriben ahí rebota.

  1. Namecheap › Domain List › koptup.com › Manage › Redirect Email (Email Forwarding) › Add Forwarder: alias ronald → tu Gmail. Si Namecheap pide elegir el servicio de correo, en Advanced DNS › Mail Settings elige Email Forwarding (gratis): agrega los MX de la raíz.
  2. Advanced DNS › Add New Record: tipo TXT Record, host _dmarc, valor v=DMARC1; p=none;. Ayuda a que Gmail confíe en el correo que sale de koptup.com sin bloquear nada.
  3. No toques los registros resend._domainkey ni send.
  4. Prueba: escribe desde otra cuenta a ronald@koptup.com; en unos minutos debe llegar a tu Gmail.

Paso 8. Gmail: que los avisos nunca vayan a spam

  1. En Gmail, en la barra de búsqueda › Mostrar opciones de búsqueda › De: avisos@koptup.com OR onboarding@resend.dev.
  2. Crear filtro › marca Nunca enviarlo a Spam (y, si quieres, Marcar siempre como importante) › Crear filtro.

Paso 9. Prueba con el botón

  1. Entra con una cuenta admin a Admin › Configuración. En la tarjeta Avisos de nuevos contactos, el correo y el WhatsApp deben decir Configurado, con el destino enmascarado; si falta algo, la tarjeta lista las variables.
  2. Pulsa Enviar aviso de prueba. Por cada canal ves lo que respondió el proveedor: «Enviado» con su id, o «Falló» con el motivo (en WhatsApp, con el código de Meta y qué revisar). Revisa tu Gmail y tu WhatsApp. Puedes hacer 5 pruebas cada 15 minutos; cada una queda en la bitácora (notifications.test).
  3. Envía un mensaje real desde www.koptup.com/contact con tu correo. En Admin › Contactos ese contacto debe decir «Correo: Enviado» y «WhatsApp: Enviado» (o «Entregado» y «Leído» con el webhook). En el correo, Responder le escribe directo a la persona.

Si algo no llega: 9.7 El correo no sale y 9.11 El WhatsApp no llega. Las pantallas del panel están en el Manual del administrador › 13.

Qué pasa después, sin que hagas nada

Mecanismo Qué hace Cómo se apaga
Reintentos Un job revisa cada 5 minutos los contactos de los últimos 3 días con un canal fallido (o pendiente hace más de 10 minutos, por ejemplo por un reinicio) y lo reenvía, con esperas de 5 min, 15 min, 1 h, 3 h y 12 h, hasta 6 intentos. Nunca reenvía un canal que ya salió, y el correo usa la misma clave de idempotencia para que Resend no lo duplique NOTIFY_JOBS_ENABLED=false
Prueba mensual Cada 30 días, entre las 9:00 y las 18:59 hora de Colombia, te llega «Prueba mensual: tus avisos de contactos funcionan» por cada canal. Si uno falla, te avisa por el otro con el error. Mantiene viva la plantilla (Meta archiva las que pasan 12 meses sin uso) y te confirma que todo sigue bien aunque no lleguen contactos NOTIFY_HEARTBEAT_DAYS=0
Alerta del webhook Si Meta informa que un WhatsApp no se entregó, te llega un correo con el código y qué revisar (una vez por mensaje) y el contacto queda «Falló», listo para reintentar Quitando las dos variables del webhook
Reenvío manual En Admin › Contactos, el admin pulsa Reenviar aviso en un contacto (o Enviar el aviso otra vez si ya salió por los dos canales) —

5. Levantar todo en local, paso a paso

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

    NAV["Navegador"]:::visitante
    WEB["Web: next dev, puerto 3000"]:::sistema
    API["Backend: nodemon, puerto 3001"]:::sistema
    MG[("MongoDB 7, puerto 27017")]:::externo
    RS[("Redis 7, puerto 6379")]:::externo
    AI["OpenAI real o mock en 127.0.0.1:3999"]:::externo

    NAV --> WEB
    NAV -->|"NEXT_PUBLIC_API_URL=http://localhost:3001"| API
    WEB -->|"middleware"| API
    API --> MG
    API --> RS
    API --> AI
Loading

El navegador habla con la web y también directamente con el backend; por eso la web debe saber la URL local del backend.

Requisitos: Node.js 20.9 o superior, npm 9 o superior y Docker (o MongoDB y Redis instalados).

  1. Clona e instala (desde la raíz; instala la web y el backend juntos):

    git clone https://github.com/ronalc90/koptup.git
    cd koptup
    NODE_ENV=development npm install
  2. Arranca MongoDB y Redis:

    docker run -d --name koptup-mongo -p 27017:27017 mongo:7
    docker run -d --name koptup-redis -p 6379:6379 redis:7

    O, con el compose del repositorio, npm run docker:up (docker-compose.dev.yml: los dos sin contraseña y publicados solo en 127.0.0.1; npm run docker:down los detiene). Redis es opcional para navegar, pero sin él la sesión no se renueva (dura 15 minutos) y las funciones de IA públicas se apagan.

  3. (Opcional) Arranca el mock de OpenAI, si no quieres gastar en la IA real. Responde «Respuesta simulada (mock de OpenAI)…» y cita el primer fragmento que recibe:

    node apps/web/e2e/support/openai-mock.js        # http://127.0.0.1:3999/v1
  4. Configura el backend: copia la plantilla y ajusta lo mínimo.

    cp apps/backend/.env.example apps/backend/.env

    En apps/backend/.env: deja MONGODB_URI=mongodb://localhost:27017/koptup_db y REDIS_URL=redis://localhost:6379; cambia JWT_SECRET y JWT_REFRESH_SECRET por dos valores largos y distintos; para la IA, pon tu OPENAI_API_KEY o, con el mock, OPENAI_BASE_URL=http://127.0.0.1:3999/v1 y cualquier clave de prueba; para «Prueba con tu documento», DEMO_UPLOAD_ENABLED=true.

    Avisos al dueño en local: no pongas la key de Resend ni el token de Meta reales (cada contacto de prueba te avisaría de verdad). Sin ellos, los contactos se guardan y el panel muestra sus avisos como «No configurado». Para verlos funcionar, arranca el mock con node apps/web/e2e/support/notify-mock.js (puerto 3998) y apunta RESEND_API_URL y WHATSAPP_GRAPH_URL a http://127.0.0.1:3998 con valores ficticios; la lista completa está en SETUP.md (sección 4). Lo que el backend «envió» se ve en http://127.0.0.1:3998/__requests.

  5. Arranca el backend:

    cd apps/backend && npm run dev                   # http://localhost:3001

    Comprueba http://localhost:3001/health (debe decir healthy y mongo: connected). En local tienes además la documentación interactiva en http://localhost:3001/api-docs.

  6. Configura la web: crea apps/web/.env.local con una sola línea:

    NEXT_PUBLIC_API_URL=http://localhost:3001

    También sirve cp apps/web/.env.example apps/web/.env.local: la plantilla apunta a http://localhost:3001. Sin el archivo, next dev usa igual http://localhost:3001. No pongas la URL de producción en .env.local: lo que pruebes crearía cuentas, solicitudes y leads reales. La URL pública solo está en apps/web/.env.production, que Next carga en next build y next start.

  7. Arranca la web:

    cd apps/web && npm run dev                       # http://localhost:3000

    El script predev regenera los mensajes agregados de las demos antes de arrancar.

  8. Crea tu cuenta admin: regístrate en http://localhost:3000/register y dale el rol de una de estas dos formas:

    • corre el script desde apps/backend (lee apps/backend/.env; una MONGODB_URI escrita en la terminal tiene prioridad):

      npx ts-node --transpile-only src/scripts/set-admin.ts tu-correo@ejemplo.co
    • o agrega ADMIN_EMAIL=tu-correo@ejemplo.co en apps/backend/.env y reinicia el backend (nodemon no se reinicia solo por cambios en .env): funciona solo si en esa base no hay ningún otro admin.

    Los dos caminos quedan en la bitácora (user.role_change con el origen).

    Cierra sesión y vuelve a entrar: el panel queda en http://localhost:3000/admin. El catálogo de 28 demos se siembra solo al arrancar el backend.

GIF: verificación del entorno local

GIF: el backend responde /health/live y /health, «Prueba con tu documento» está encendida, la web corre en local y responde con el mock de OpenAI.

Respuesta del mock de OpenAI en «Prueba con tu documento», anotada

Con el mock, la IA es simulada pero el recorrido es real: subida, fragmentos, búsqueda y cita.

  1. Respuesta simulada: el mock repite los fragmentos que recibió; con una clave real aquí aparece la respuesta del modelo.
  2. Fuentes: el fragmento citado con su página, igual que en producción.
  3. Siguiente pregunta: cada documento admite 10.

Modo producción en local (igual que CI). Para reproducir exactamente lo que corre en Vercel y Railway, compila y arranca con NODE_ENV=production. Los comandos completos, con los valores de prueba, están en el README.md (sección «E2E en local, paso a paso»): backend con npm run build y node dist/index.js, y web con NEXT_PUBLIC_API_URL=http://localhost:3001 npm run build y npx next start -p 3000. Así se hicieron las capturas de esta documentación (web en el puerto 3500 y backend en el 3501).


6. Pruebas: unitarias, de integración y de punta a punta

Tipo Dónde viven Cuántas hay Cómo se corren Qué necesitan
Unitarias del backend apps/backend/src/__tests__/unit/, src/services/__tests__/, src/utils/__tests__/ 23 archivos (incluidos Resend, WhatsApp y el armado de los avisos, con fetch simulado) npm test --workspace=apps/backend Nada
Integración del backend apps/backend/src/__tests__/integration/ (permisos, demos, chatbot, topes de gasto, perfil, privacidad, avisos al dueño, operación…) 19 archivos MONGODB_URI_TEST=mongodb://127.0.0.1:27017/koptup_test REDIS_URL_TEST=redis://127.0.0.1:6379/15 npm test --workspace=apps/backend MongoDB y Redis solo de pruebas (cada archivo crea y borra su base; se borran claves de Redis)
Web apps/web/src/**/*.test.ts(x) (humo de las demos, middleware de acceso, catálogo, tarjeta de avisos) 70 archivos npm test --workspace=apps/web Nada (no salen a la red)
Punta a punta apps/web/e2e/*.spec.ts 13 archivos, en escritorio, móvil (Pixel 7) y un proyecto «catalogo» npm run test:e2e con la web y el backend ya levantados MongoDB, Redis, mock de OpenAI, mock de Resend y WhatsApp, backend y web compilados

Atajos desde la raíz: npm test (Jest de los dos), npm run lint, npm run typecheck y npm run test:e2e.

Punta a punta en local, en cuatro terminales (los valores son solo de prueba):

  1. MongoDB, Redis, el mock de OpenAI (paso 2 y 3 de la sección 5) y el mock de Resend y WhatsApp: node apps/web/e2e/support/notify-mock.js (puerto 3998).
  2. Backend compilado: npm run build --workspace=apps/backend y luego, en apps/backend, node dist/index.js con PORT=3001 NODE_ENV=production, una base propia (koptup_e2e), los dos JWT de prueba, CORS_ORIGIN y FRONTEND_URL en http://localhost:3000, el mock en OPENAI_BASE_URL, DEMO_UPLOAD_ENABLED=true, RATE_LIMIT_MAX_REQUESTS=10000 (todo sale de la misma máquina) y los avisos apuntando al mock de avisos con valores ficticios (RESEND_API_URL y WHATSAPP_GRAPH_URL en http://127.0.0.1:3998, más NOTIFY_JOBS_ENABLED=false; los valores exactos están en el README.md). Nunca con las credenciales reales: cada contacto de prueba avisaría de verdad.
  3. Web compilada: NEXT_PUBLIC_API_URL=http://localhost:3001 npm run build --workspace=apps/web y npx next start -p 3000 en apps/web.
  4. npx playwright install chromium (la primera vez) y E2E_BASE_URL=http://localhost:3000 E2E_API_URL=http://localhost:3001 E2E_MONGODB_URI=mongodb://127.0.0.1:27017/koptup_e2e E2E_NOTIFY_MOCK_URL=http://127.0.0.1:3998 npm run test:e2e.

La suite siembra sus propios datos por la API con emails en el dominio reservado .test, crea una cuenta admin con una contraseña al azar en cada corrida y se niega a correr contra una web o un backend que no sean locales (salvo E2E_ALLOW_REMOTE=1, solo para un entorno desechable). Nunca la corras contra producción: cambia el catálogo de demos. Para correr un solo archivo: npm run test:e2e -- e2e/demo-flow.spec.ts --project=escritorio.


7. Salud del sistema, monitoreo y registros

Señal Dónde Qué te dice
GET /health Backend 200 healthy con mongo: connected, o 503 si MongoDB no está conectado. Es el chequeo que usa Railway al desplegar (un despliegue sin base no entra en servicio y el anterior sigue atendiendo) y el que revisa el monitoreo
GET /health/live Backend 200 {"status":"alive"} si el proceso responde, aunque no haya base. Sirve para distinguir «el proceso no arranca» de «no hay base de datos»
Monitoreo (workflow Monitoreo) GitHub Actions, cada 15 minutos Si https://www.koptup.com y <BACKEND_HEALTH_URL> responden. Si algo falla tres veces seguidas en la misma corrida, la corrida queda en rojo y GitHub envía un correo
GET /api/demo-rag/status Backend Si «Prueba con tu documento» está encendida y por qué no (disabled, unavailable, budget_exhausted)
GET /api/demo-catalog Backend El catálogo de demos que ve la web; si falla, la web usa su tabla de respaldo
Registros del servicio Railway Cada petición (método, ruta, estado, IP y navegador), los avisos [env] del arranque, una línea por canal de avisos (Avisos por correo: …, Avisos por WhatsApp: …, con el destino enmascarado), cada envío o fallo de correo y WhatsApp (sin la key ni el token) y los errores
Admin › Configuración › Avisos de nuevos contactos (GET /api/admin/notifications/status) Panel (admin y manager) Si cada canal está configurado o qué variable falta, el proveedor, el destino enmascarado, el último aviso enviado, el último fallo con su error y la próxima prueba mensual. Nunca muestra keys ni tokens
Registros del despliegue Vercel Errores de compilación y de las funciones del servidor de la web

Estado de «Prueba con tu documento», anotado

Respuesta real de GET /api/demo-rag/status en el entorno local.

  1. enabled: si la función está encendida.
  2. reason: por qué no lo está (disabled, unavailable o budget_exhausted); null si está encendida.
  3. budgetExhausted: si se alcanzó el tope del mes (nunca muestra montos).
  4. Límites públicos: 3 documentos por IP al día, 10 preguntas por documento, 60 minutos de vida, 5 MB y 30 páginas.

Las respuestas de /health y del catálogo, con capturas, están en Arquitectura.

7.1 Monitoreo con GitHub Actions

El workflow .github/workflows/monitor.yml corre cada 15 minutos (y a mano desde la pestaña Actions) el script scripts/uptime-check.sh:

  1. GET https://www.koptup.com debe responder 2xx o 3xx (sigue redirecciones).
  2. GET <BACKEND_HEALTH_URL> debe responder 200. Un 503 cuenta como caída: la API está arriba pero sin base de datos, y el sitio no puede iniciar sesiones ni guardar solicitudes.
  3. Cada URL se intenta tres veces, con 15 segundos entre intentos, para no alertar por un corte de segundos.
flowchart LR
    classDef sistema fill:#e0e7ff,stroke:#4f46e5,color:#312e81
    classDef error fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
    classDef admin fill:#fef3c7,stroke:#d97706,color:#78350f
    classDef decision fill:#fef3c7,stroke:#d97706,color:#0f172a

    C["Cada 15 min: workflow Monitoreo"]:::sistema --> W{"¿La web responde?"}:::decision
    W -->|"Sí"| V{"¿BACKEND_HEALTH_URL definida?"}:::decision
    W -->|"No, 3 intentos"| F["Corrida en rojo"]:::error
    V -->|"No"| A["Aviso en la corrida: API sin monitorear"]:::admin
    V -->|"Sí"| H{"¿/health responde 200?"}:::decision
    H -->|"Sí"| OK["Corrida en verde"]:::sistema
    H -->|"No, 3 intentos"| F
    F --> M["GitHub envía un correo"]:::admin
Loading

Qué revisa cada corrida del monitoreo y cuándo avisa.

Para que funcione (una sola vez, ver acciones pendientes):

  • Crea la variable del repositorio BACKEND_HEALTH_URL en Settings › Secrets and variables › Actions › Variables con la URL pública de la API terminada en /health. Es una variable, no un secreto: la URL ya es pública. Sin ella el workflow solo revisa la web y deja un aviso.
  • Revisa que tu cuenta reciba los correos de Actions (Settings › Notifications › Actions de tu perfil): GitHub avisa de las corridas programadas fallidas a la cuenta que creó el workflow o cambió por última vez su programación (cron), normalmente quien fusiona este cambio en main.

Ten en cuenta: los workflows programados solo corren desde main (empieza a funcionar cuando este archivo llegue a main); GitHub puede atrasar unos minutos las corridas programadas; y si el repositorio pasa 60 días sin actividad, GitHub pausa el workflow y hay que reactivarlo en Actions › Monitoreo › Enable workflow. Cuando llegue una alerta, sigue el runbook 9.10.

Para probar el chequeo contra tu entorno local: scripts/uptime-check.sh http://localhost:3000 http://localhost:3001/health. El backend también escribe logs/error.log y logs/combined.log, pero en Railway el disco se borra con cada despliegue: confía en los registros de la plataforma.


8. Cómo se publica esta wiki

La fuente de la wiki está versionada en docs/wiki/ y el workflow Publicar wiki (.github/workflows/wiki-sync.yml) la copia completa a la wiki de GitHub.

sequenceDiagram
    autonumber
    actor D as Desarrollador
    participant G as GitHub (repositorio)
    participant A as Actions: Publicar wiki
    participant W as Wiki de GitHub

    D->>G: push con cambios en docs/wiki a main (o ejecución manual)
    G->>A: dispara el workflow (uno a la vez, sin cancelar el anterior)
    A->>W: clona el repositorio .wiki.git con GITHUB_TOKEN
    alt la wiki no está activada
        W-->>A: error al clonar
        A-->>D: falla con el mensaje para activarla
    else wiki activa
        A->>A: borra todo menos .git y copia docs/wiki
        A->>A: quita la extensión .md de los enlaces entre páginas
        alt hay cambios
            A->>W: commit y push
            W-->>D: wiki publicada
        else sin cambios
            A-->>D: termina sin publicar
        end
    end
Loading

La publicación tarda unos 15 segundos. También se puede lanzar a mano desde la pestaña Actions.

Reglas para quien edita:

  • Edita siempre en docs/wiki/, nunca en la web de la wiki: la siguiente publicación borra lo que se haya cambiado allá.
  • Enlaza entre páginas con [Texto](Nombre-Pagina); el workflow quita el .md al publicar. Las imágenes van en docs/wiki/images/.
  • Valida los diagramas Mermaid antes de subir (la wiki de GitHub los dibuja; un error de sintaxis se ve como texto).
  • El workflow se dispara solo con push a main, así que la wiki muestra lo que ya está en main. Para publicar otra rama (por ejemplo, para revisar una documentación antes de fusionarla), lánzalo a mano desde Actions › Publicar wiki › Run workflow eligiendo esa rama: la siguiente publicación desde main vuelve a dejarla igual a main.

9. Runbooks: qué hacer cuando algo falla

Cada runbook dice el síntoma, la causa probable, los pasos y cómo verificar. Los diagramas de decisión de varios de ellos están en Flujos de administración y operación › 9; las pantallas del panel, en Manual del administrador › 15.

flowchart LR
    classDef error fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
    classDef admin fill:#fef3c7,stroke:#d97706,color:#78350f

    S1["No funciona el login, el contacto ni las solicitudes"]:::error --> R1["9.1 Backend caído"]:::admin
    S2["Un secreto se filtró o hay que cambiarlo"]:::error --> R2["9.2 Rotar secretos"]:::admin
    S3["Prueba con tu documento dice que no está disponible"]:::error --> R3["9.3 Encenderla"]:::admin
    S4["Nadie puede entrar al panel"]:::error --> R4["9.4 Crear o recuperar el admin"]:::admin
    S5["Hay que abrir o cerrar una demo"]:::error --> R5["9.5 Cambiar el modo"]:::admin
    S6["Una demo con IA habla de cupo agotado"]:::error --> R6["9.6 Presupuesto de IA"]:::admin
    S7["Un correo o un aviso por correo no llega"]:::error --> R7["9.7 El correo no sale"]:::admin
    S8["Una demo dice No se pudo verificar o En mantenimiento"]:::error --> R8["9.8 Demo no disponible"]:::admin
    S9["El último despliegue rompió algo"]:::error --> R9["9.9 Revertir"]:::admin
    S10["Llegó un correo del monitoreo"]:::error --> R10["9.10 Caída detectada"]:::admin
    S11["El aviso por WhatsApp no llega"]:::error --> R11["9.11 El WhatsApp no llega"]:::admin
Loading

Busca el síntoma a la izquierda y ve al runbook de la derecha.

9.1 El backend está caído (situación actual)

Síntoma. La web pública carga, pero: el inicio de sesión falla y /admin devuelve al login; el formulario de contacto y /solicitar-demo muestran error; las demos «Con solicitud» dicen «No se pudo verificar» a cualquier visitante; «Prueba con tu documento» no está disponible.

Situación al 9 de octubre de 2026. La URL de la API configurada en la web responde 404 con este cuerpo, que es la respuesta de Railway cuando no hay ningún servicio detrás del dominio:

{"status":"error","code":404,"message":"Application not found"}

Pasos

  1. Confirma el síntoma. Abre en el navegador <URL de la API>/health/live. La URL es el valor de NEXT_PUBLIC_API_URL en Vercel o, si no está, el de apps/web/.env.production.
    • Application not found → sigue con el paso 2.
    • Error 5xx o no responde → ve al paso 5.
    • {"status":"alive"} → la API vive; ve al paso 7.
  2. Revisa el proyecto en Railway. ¿Existe el servicio del backend? ¿Está pausado, eliminado o con la facturación vencida? ¿Cambió su dominio público (Settings › Networking)?
  3. Si el servicio no existe, créalo desde GitHub:
    1. New service › GitHub repo con este repositorio y la rama main.
    2. Directorio raíz apps/backend (ahí está railway.json, que define build, arranque y chequeo de salud).
    3. Agrega un Redis al proyecto y copia su URL en REDIS_URL.
    4. Carga las variables de la sección 4.3. Mínimo: NODE_ENV=production, MONGODB_URI, JWT_SECRET, JWT_REFRESH_SECRET, FRONTEND_URL=https://www.koptup.com, TRUST_PROXY_HOPS=1, OPENAI_API_KEY, ADMIN_EMAIL y las de los avisos al dueño (4.5). Aprovecha para usar secretos nuevos (9.2) y deja apagado Serverless en Settings.
    5. Genera un dominio público para el servicio.
    6. Monta un volumen en /app/uploads para que los archivos subidos no se borren en cada despliegue, y pon <dominio>/health en la variable BACKEND_HEALTH_URL del repositorio (acciones pendientes).
  4. Si el dominio de la API cambió: en Vercel (proyecto koptup, entorno Production) pon la nueva URL en NEXT_PUBLIC_API_URL y redespliega la web (Deployments › ⋯ › Redeploy). La misma variable alimenta la CSP, así que no hay que tocar código. Después actualiza también apps/web/.env.production en el repositorio para que no quede desalineado.
  5. Si responde 5xx o se reinicia en bucle: abre los registros del despliegue en Railway.
    • Configuración incompleta: MONGODB_URI, JWT_SECRET → falta esa variable.
    • Avisos [env] … → no detienen el arranque, pero dicen qué función quedó apagada.
    • Otro error al arrancar justo después de un despliegue → revierte (9.9).
  6. Espera el chequeo de Railway: el despliegue entra en servicio cuando /health responde 200 (con MongoDB conectado) antes de 120 segundos. Si no lo logra, Railway marca el despliegue como fallido y deja el anterior; revisa MONGODB_URI y que la base acepte conexiones desde Railway.
  7. Verifica de afuera hacia adentro:
    1. GET /health → 200 con mongo: connected. Si da 503, revisa MONGODB_URI y que la base acepte conexiones desde Railway.
    2. GET /api/demo-catalog → lista de 28 demos.
    3. En www.koptup.com: inicia sesión en /admin, envía una prueba desde /contact (te debe llegar el aviso por correo y WhatsApp, y en Admin › Contactos los dos canales deben decir «Enviado») y abre /demo/erp sin sesión: debe mostrar «Solicitar acceso», no «No se pudo verificar».
    4. Si el navegador muestra errores de CORS, revisa que la web use https://www.koptup.com o agrega su dominio en CORS_ORIGIN.

«No se pudo verificar» en una demo con acceso controlado, anotada

Lo que ve hoy un visitante en cualquier demo «Con solicitud» de www.koptup.com (reproducido en local).

  1. Estado de la pantalla de acceso: «No se pudo verificar».
  2. Mensaje: el servicio no respondió.
  3. Intentar de nuevo: vuelve a cargar la demo.
  4. Solicitar acceso: lleva al formulario, que también necesita la API.

9.2 Rotar secretos

Cuándo. Si un secreto pudo quedar expuesto, si se va alguien con acceso o como rutina. Hoy es urgente para la contraseña de MongoDB y los dos JWT, que estuvieron en archivos del repositorio público y siguen en su historial (ver Estado y próximos pasos).

Genera valores largos y aleatorios en tu computador, por ejemplo:

node -e "console.log(require('crypto').randomBytes(48).toString('base64url'))"
Secreto Dónde se cambia Efecto en los usuarios
Contraseña de MongoDB En el proveedor de la base y luego MONGODB_URI en Railway Ninguno, si la API vuelve a arrancar bien
JWT_SECRET Railway Los tokens de acceso dejan de valer; la web los renueva sola con el de renovación. Nadie lo nota
JWT_REFRESH_SECRET Railway Nadie puede renovar: todos vuelven a iniciar sesión en máximo 15 minutos
INTERNAL_API_KEY Vercel y Railway, mismo valor Ninguno. Mientras no coincidan, los cupos por IP de esas peticiones cuentan con la IP de Vercel
RESEND_API_KEY, SMTP_PASS, OPENAI_API_KEY, WHATSAPP_API_TOKEN, WHATSAPP_APP_SECRET, WHATSAPP_WEBHOOK_VERIFY_TOKEN Revoca la vieja en el proveedor (Resend › API Keys; Meta › usuario del sistema › Revocar tokens; la clave secreta de la app se restablece en Configuración de la app › Básica), crea una nueva y cámbiala en Railway. Si cambias el verify token, escríbelo igual en el webhook de Meta Ninguno. Mientras el token de Meta o la key de Resend no coincidan, los avisos de ese canal fallan y el job los reintenta

Pasos

  1. Cambia primero en el proveedor (por ejemplo, la contraseña del usuario de MongoDB o una clave nueva de OpenAI).
  2. Actualiza la variable en Railway (y en Vercel si es INTERNAL_API_KEY). Usa valores distintos para los dos JWT.
  3. Redespliega (Railway lo propone al guardar; Vercel exige Redeploy).
  4. Verifica: GET /health en 200, inicio de sesión en /admin, Enviar aviso de prueba en Admin › Configuración (correo y WhatsApp) y una pregunta en /demo/chatbot.
  5. Revoca la credencial vieja en el proveedor cuando todo funcione.

Los enlaces de activación pendientes no se afectan: se guardan como un hash en MongoDB y no dependen de los JWT. GitHub Actions no usa secretos propios (la wiki se publica con el GITHUB_TOKEN automático).

9.3 Encender «Prueba con tu documento»

Síntoma. En /demo/chatbot, la pestaña «Prueba con tu documento» muestra «La prueba con tu documento no está disponible en este momento» (la pantalla está en Flujos del visitante › Paso 8).

La función falla cerrada: solo se enciende si se cumplen las tres condiciones.

Condición Variable Si falta, reason dice
Encendido explícito DEMO_UPLOAD_ENABLED=true disabled
Redis conectado REDIS_URL unavailable
Clave de OpenAI OPENAI_API_KEY unavailable
Presupuesto disponible este mes DEMO_MONTHLY_BUDGET_USD (50 por defecto) budget_exhausted

Pasos

  1. Consulta GET <URL de la API>/api/demo-rag/status y lee reason.
  2. En Railway, define lo que falte: DEMO_UPLOAD_ENABLED=true, REDIS_URL (Redis del proyecto) y OPENAI_API_KEY. Recomendadas: TRUST_PROXY_HOPS=1 (cupo de 3 documentos por IP al día), MONGODB_URI (guarda el email como lead con origen demo-rag) y, si quieres, DEMO_MONTHLY_BUDGET_USD.
  3. Redespliega la API.
  4. Verifica: status debe responder "enabled": true y "reason": null (captura de la sección 7). Luego sube un PDF corto en /demo/chatbot?mode=upload y haz una pregunta.

Para apagarla, pon DEMO_UPLOAD_ENABLED=false (o bórrala) y redespliega. La demo con los documentos de ejemplo sigue funcionando.

9.4 Crear o recuperar el admin

Hay tres vías para dar el rol admin y ninguna se puede hacer desde fuera del sistema: la API no deja que nadie se asigne ese rol.

Vía Cuándo usarla Cómo
ADMIN_EMAIL Primer admin o recuperar el panel cuando no queda ningún admin Variable en Railway; al arrancar, la API le da el rol admin a esa cuenta solo si existe, está activa y no hay ningún otro admin. Queda en la bitácora
Script set-admin Dar el rol a otra cuenta sin redesplegar railway run npx ts-node --transpile-only src/scripts/set-admin.ts <email> desde apps/backend (Railway inyecta MONGODB_URI; en local el script lee apps/backend/.env). Queda en la bitácora
Admin › Usuarios Ya tienes un admin Cambia el rol en el panel (Manual del administrador › 7)

railway run ejecuta el comando en tu computador con las variables del servicio: necesitas la CLI de Railway, haber corrido npm install y que la base de datos acepte conexiones desde tu red.

Crear el primer admin

  1. Registra la cuenta en https://www.koptup.com/register (o usa una que ya exista).
  2. Pon su email en ADMIN_EMAIL en Railway y redespliega. En el registro del arranque debe aparecer «Rol admin asignado a ADMIN_EMAIL (no había ningún admin)». Si dice «ADMIN_EMAIL no corresponde a ningún usuario registrado», la cuenta no existe todavía; si dice «ya existe otra cuenta admin», usa el script set-admin o el panel.
  3. Cierra sesión y vuelve a entrar: el panel decide qué mostrar con el rol guardado al iniciar sesión.

Olvidaste la contraseña

  1. Usa «¿Olvidaste tu contraseña?» en /login. Llega un enlace que vale 1 hora y sirve una sola vez. Necesita correo configurado (9.7).
  2. Si el correo no está configurado, el enlace no llega y no hay forma de verlo en el panel: registra otra cuenta y conviértela en admin con el script set-admin (ADMIN_EMAIL no sirve aquí, porque la cuenta vieja sigue siendo admin). Desde ella puedes seguir operando.
  3. Las cuentas creadas con Google no tienen contraseña: entra con «Continuar con Google» (requiere GOOGLE_CLIENT_ID y GOOGLE_CLIENT_SECRET).

Alguien le quitó el rol a la cuenta de ADMIN_EMAIL: si queda otro admin, el arranque no se lo devuelve (así una decisión tomada desde el panel no se deshace sola); pídeselo a ese admin o usa el script. Si no queda ningún admin, el siguiente arranque se lo devuelve y lo deja en la bitácora.

9.5 Cambiar el modo de una demo

Cuándo. Quieres que una demo sea abierta, que pida acceso o que sea solo por invitación, o apagarla («En mantenimiento»).

  1. Entra con una cuenta admin (los demás roles ven el catálogo en modo consulta) a Admin › Catálogo de demos (/admin/catalogo-demos).
  2. En la fila de la demo, elige el modo (Abierta, Requiere acceso o Solo por invitación) o apaga Activa.
  3. Pulsa Guardar en esa fila. El cambio queda en la bitácora con tu usuario.
  4. Espera hasta un minuto: la web guarda el catálogo en memoria 60 segundos.
  5. Verifica en una ventana privada: abre /demo/<slug>. Debe abrir la demo (Abierta), mostrar el candado (Requiere acceso o Solo por invitación) o «En mantenimiento» (apagada).

No hace falta desplegar: el modo vive en MongoDB. Ten en cuenta que el chatbot RAG es fijo (siempre abierto y activo), que los accesos ya concedidos se respetan y que el sitemap no cambia (lista las demos abiertas según la semilla del código, no el catálogo; ver SEO). El paso a paso con capturas y GIF está en Manual del administrador › 6.

9.6 Se agotó el presupuesto de IA

Síntoma. Según la función: el chatbot responde en modo extractivo y lo dice; «Prueba con tu documento» muestra «Alcanzamos el cupo de pruebas de este mes»; LinkedIn Ads y el gestor de contenido dicen que la IA no está disponible.

Función Variable del tope Por defecto Clave en Redis
Chatbot y asistentes de mesa de ayuda, HRMS y LMS CHATBOT_MONTHLY_BUDGET_USD 50 USD chatbot:spend:AAAA-MM
«Prueba con tu documento» DEMO_MONTHLY_BUDGET_USD 50 USD demo-rag:spend:AAAA-MM
Generador de LinkedIn Ads LINKEDIN_ADS_MONTHLY_BUDGET_USD 20 USD linkedin-ads:spend:AAAA-MM
Gestor de contenido CONTENT_MONTHLY_BUDGET_USD 20 USD content:spend:AAAA-MM
Gestor de documentos DOCUMENTS_MONTHLY_BUDGET_USD 10 USD documents:spend:AAAA-MM
APIs de salud (cuentas médicas, auditoría, sistema experto, CUPS y reglas de facturación) HEALTH_MONTHLY_BUDGET_USD 30 USD health:spend:AAAA-MM

Pasos

  1. Confirma que es el cupo y no otra cosa. Para la demo con documento, GET /api/demo-rag/status dice budget_exhausted. Si el mensaje habla de «no disponible», revisa primero OPENAI_API_KEY y REDIS_URL: sin Redis no se puede medir el gasto y la IA se apaga.
  2. Revisa el gasto real en la cuenta de OpenAI (el panel de KopTup no lo muestra) y, si quieres el número exacto del sistema, la clave del mes en Redis.
  3. Decide:
    • Subir el tope: cambia la variable en Railway (un número en dólares) y redespliega. Asegúrate de que la cuenta de OpenAI tenga saldo y límites suficientes.
    • Esperar: el contador se reinicia el día 1 del mes UTC (7:00 p. m. del último día del mes, hora de Colombia).
  4. Verifica con una pregunta en la demo afectada.

Un valor inválido o negativo en la variable deja la función en 0, es decir, apagada.

9.7 El correo no sale

Síntoma. No te llega el aviso de un contacto nuevo por correo, o a un prospecto no le llega el acuse, la aprobación con su enlace, el recordatorio de vencimiento o el enlace para restablecer la contraseña.

Causa más común. El backend intenta enviar por SMTP desde Railway Free, Trial o Hobby: esos planes bloquean el SMTP saliente (puertos 25, 465 y 587), la conexión se queda colgada hasta el tiempo de espera y el correo no sale. La solución es RESEND_API_KEY: con ella todo el correo sale por la API HTTPS de Resend, que Railway no bloquea.

Mientras tanto el negocio sigue: el contacto queda guardado en Admin › Contactos (con el aviso por WhatsApp, si está configurado), y cada aprobación o acceso concedido muestra el enlace de activación para copiarlo o enviarlo por WhatsApp (Manual del administrador › 4.4). Ninguna acción del panel falla por el correo.

flowchart TD
    classDef error fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
    classDef admin fill:#fef3c7,stroke:#d97706,color:#78350f
    classDef sistema fill:#e0e7ff,stroke:#4f46e5,color:#312e81

    Q["No llega un correo"]:::error --> C{"Admin › Configuración:<br/>¿Correo configurado?"}
    C -->|"No: falta RESEND_API_KEY<br/>o NOTIFY_EMAIL"| F1["Define lo que falta en Railway<br/>y redespliega (4.5, pasos 1 y 5)"]:::admin
    C -->|"Sí, pero el proveedor es SMTP"| F2["Railway Hobby bloquea SMTP:<br/>define RESEND_API_KEY"]:::admin
    C -->|"Sí, con Resend"| T["Enviar aviso de prueba"]:::sistema
    T --> R{"¿Qué responde el correo?"}
    R -->|"Falló: Resend 401 o 403"| F3["Key revocada o sin permiso:<br/>crea otra con Sending access"]:::admin
    R -->|"Falló: dominio no verificado"| F4["Revisa koptup.com en Resend › Domains<br/>y los registros DNS de Resend"]:::admin
    R -->|"Falló: sin respuesta o 5xx"| F5["Falla temporal: el job reintenta<br/>cada 5 min, hasta 6 intentos"]:::sistema
    R -->|"Enviado con id"| G{"¿Está en tu bandeja?"}
    G -->|"No"| F6["Busca en spam y crea el filtro<br/>de Gmail (4.5, paso 8)"]:::admin
    G -->|"Sí"| OK["El canal funciona: revisa el<br/>contacto en Admin › Contactos"]:::sistema
Loading

Pasos

  1. Mira la tarjeta en Admin › Configuración › Avisos de nuevos contactos (admin y manager):
    • «No configurado» con la lista de lo que falta → define RESEND_API_KEY y NOTIFY_EMAIL (o ADMIN_EMAIL) en Railway (4.5, pasos 1 y 5).
    • Proveedor SMTP con la advertencia de Railway → define RESEND_API_KEY; SMTP queda como respaldo solo sin ella.
    • «Último error» → el motivo del último fallo, sin la key y con los correos enmascarados.
  2. Pulsa «Enviar aviso de prueba» (solo admin) y lee el resultado del correo:
    • Resend 401 o Resend 403 (key inválida, revocada o sin permiso) → crea una key nueva con Sending access y cámbiala en Railway.
    • 403 que menciona el dominio o la verificación → koptup.com no está verificado en esa cuenta de Resend. El aviso al dueño se reintenta solo desde onboarding@resend.dev, que solo entrega al correo dueño de la cuenta de Resend (el panel lo marca como respaldo). Revisa Domains en Resend y no borres los registros resend._domainkey ni send de Namecheap.
    • 422 (validación) → revisa el formato de EMAIL_FROM (Nombre <correo@koptup.com>) y de NOTIFY_EMAIL.
    • 429 o 5xx o «sin respuesta en 15 s» → falla temporal: el backend ya reintentó una vez y el job lo vuelve a intentar.
    • «Enviado» con un id → Resend lo aceptó. Si no está en tu bandeja, busca en spam y crea el filtro de Gmail (4.5, paso 8); en Resend › Emails ves si se entregó o rebotó.
  3. Mira el contacto en Admin › Contactos: el detalle dice por canal el estado, el motivo del fallo, los intentos y el próximo reintento automático. Reenviar aviso (admin) lo intenta otra vez.
  4. En los registros de Railway busca Correo " (cada envío dice enviado por resend a ro***@… o falló por resend: …) y los avisos [env] RESEND_API_KEY … del arranque.
  5. Revisa FRONTEND_URL: si un correo llega con un enlace a localhost, falta https://www.koptup.com.
  6. Recordatorios de vencimiento: además del correo, el job debe estar encendido (DEMO_GRANTS_JOB_ENABLED distinto de false); corre cada hora y avisa 3 días antes.

Para un prospecto, el detalle de la solicitud o del acceso dice junto al enlace si el correo «Se envió», «No está configurado» o «No se pudo enviar». La versión en diagrama de decisión, con los demás correos, está en Flujos de administración › 9.2.

9.8 Una demo muestra «No se pudo verificar» o «En mantenimiento»

Son dos estados de la pantalla de acceso de las demos (/demo-acceso). Los demás estados («Sin sesión», «Sin acceso», «Acceso vencido», «Acceso retirado») los resuelve el equipo desde el panel: ver Flujos de administración › 9.3.

Lo que ve la persona Causa Qué hacer
«No se pudo verificar» («No pudimos verificar tu acceso en este momento porque el servicio no respondió») La web no obtuvo respuesta del backend al consultar el catálogo o el acceso (caído, lento más allá del tiempo de espera o con error) Es el runbook 9.1. Cuando la API vuelve, la web reintenta el catálogo cada 10 segundos: la persona pulsa Intentar de nuevo
«En mantenimiento» La demo está desactivada en el catálogo Si fue a propósito, nada. Si no, enciende Activa en Admin › Catálogo de demos (9.5) y espera hasta un minuto
«La prueba con tu documento no está disponible en este momento» Función apagada, sin Redis u OpenAI, o sin presupuesto Runbook 9.3 o 9.6

Mientras el backend no responde, las demos abiertas siguen funcionando con sus datos de ejemplo: la web usa una tabla de respaldo con el modo de cada demo. Las que tienen IA real no responden.

«En mantenimiento», anotada

Una demo desactivada desde el catálogo.

  1. Estado «En mantenimiento».
  2. Ver otras demos: vuelve al catálogo.
  3. Solicitar demo guiada: abre el formulario con esa demo elegida.

«No se pudo verificar» en el celular

La misma pantalla en un celular (390 × 844).

9.9 Revertir un despliegue

Cuándo. Un despliegue nuevo rompió algo y necesitas volver atrás ya.

Web (Vercel), en minutos

  1. Abre el proyecto koptup en Vercel › Deployments y filtra por Production.
  2. Ubica el último despliegue de producción que funcionaba (cada uno muestra el commit y el mensaje del merge).
  3. En su menú elige Instant Rollback (o Promote to Production). El dominio pasa a ese despliegue sin volver a compilar.
  4. Verifica www.koptup.com y la página que fallaba.
  5. Importante: el siguiente push a main vuelve a desplegar el código con el error. Corrígelo en main (siguiente sección) antes de fusionar otra cosa.

Si la causa fue una variable y no el código, restáurala en Vercel y redespliega: un rollback no cambia variables, y las NEXT_PUBLIC_* quedan fijas en el despliegue al que vuelves.

API (Railway)

  1. En el servicio del backend › Deployments, elige el despliegue anterior y usa Redeploy (o Rollback).
  2. Espera el chequeo /health de Railway y verifica GET /health desde el navegador.

Corregir main (definitivo)

  1. Crea una rama y ejecuta git revert -m 1 <commit del merge>.
  2. Abre un PR, espera CI en verde y fusiona. Vercel y Railway despliegan el código revertido.
  3. Si el cambio había escrito datos en MongoDB, revísalos a mano: revertir el código no deshace datos (no hay migraciones automáticas; el catálogo de demos solo agrega lo que falta al arrancar).

El diagrama de este procedimiento está en Flujos de administración › 9.6.

9.10 El monitoreo avisó una caída

Síntoma. Llega un correo de GitHub «Run failed: Monitoreo» o la pestaña Actions muestra la corrida en rojo.

  1. Abre la corrida y lee el paso «Revisar la web y la salud de la API»: dice qué URL falló y qué respondió (000 = no hubo respuesta; 503 en /health = API sin base de datos; 404 con «Application not found» = Railway no tiene servicio en ese dominio).
  2. Confirma desde tu navegador la misma URL. Si ya responde, fue un corte breve: lanza el workflow a mano (Run workflow) para confirmar que vuelve a verde.
  3. Si la web no responde: revisa el estado del despliegue en Vercel (proyecto koptup) y, si el último despliegue la rompió, revierte.
  4. Si la API no responde o da 503: sigue el runbook 9.1 desde el paso 1.
  5. Cuando vuelva, la siguiente corrida programada queda en verde; GitHub no envía correo de recuperación.

9.11 El WhatsApp no llega

Síntoma. Llega el correo de un contacto nuevo, pero no el WhatsApp; o Admin › Contactos muestra «WhatsApp: Falló» o «No configurado».

Por qué pasa casi siempre. El aviso se envía con la plantilla koptup_aviso de la WhatsApp Cloud API de Meta. Falla si falta una variable, si el token venció (el temporal del panel dura horas), si la plantilla no está aprobada o no coincide (nombre, idioma, 2 variables) o si tu número no está en la lista de destinatarios del número de prueba. Twilio y UltraMsg ya no se soportan: si WHATSAPP_PROVIDER vale twilio o ultramsg, el canal queda apagado.

flowchart TD
    classDef error fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
    classDef admin fill:#fef3c7,stroke:#d97706,color:#78350f
    classDef sistema fill:#e0e7ff,stroke:#4f46e5,color:#312e81

    Q["No llega el WhatsApp"]:::error --> C{"Admin › Configuración:<br/>¿WhatsApp configurado?"}
    C -->|"No: falta una variable<br/>o WHATSAPP_PROVIDER no es meta"| F1["Define token, id del número y<br/>ADMIN_WHATSAPP_NUMBER (4.5, paso 5)"]:::admin
    C -->|"Sí"| T["Enviar aviso de prueba"]:::sistema
    T --> R{"¿Qué responde WhatsApp?"}
    R -->|"Falló: código 190"| F2["Token vencido: genera el de<br/>usuario del sistema, caducidad Nunca"]:::admin
    R -->|"Falló: código 131030"| F3["Agrega y verifica tu número<br/>en la lista del número de prueba"]:::admin
    R -->|"Falló: código 132xxx"| F4["Revisa la plantilla koptup_aviso:<br/>aprobada, idioma es, texto exacto"]:::admin
    R -->|"Enviado con respaldo hello_world"| F4
    R -->|"Enviado"| W{"¿Te llegó al celular?"}
    W -->|"No"| F5["Activa el webhook para saber<br/>si Meta lo entregó (4.5, paso 6)"]:::admin
    W -->|"Sí"| OK["El canal funciona"]:::sistema
Loading

Pasos

  1. Mira la tarjeta en Admin › Configuración › Avisos de nuevos contactos: si WhatsApp dice «No configurado», la lista dice qué variable falta (WHATSAPP_API_TOKEN, WHATSAPP_PHONE_NUMBER_ID o ADMIN_WHATSAPP_NUMBER; o WHATSAPP_PROVIDER si pide un proveedor retirado). El registro del arranque dice lo mismo en la línea Avisos por WhatsApp: ….

  2. Pulsa «Enviar aviso de prueba» (solo admin). Si falla, el resultado trae el código de Meta y qué revisar:

    Código de Meta Qué significa Qué hacer
    190 El token venció o es inválido Genera el token del usuario del sistema con caducidad Nunca (4.5, paso 4) y cámbialo en Railway
    10 o 200 La app no tiene el permiso Al generar el token, marca whatsapp_business_messaging y whatsapp_business_management, y asigna la app y la cuenta de WhatsApp al usuario del sistema
    100 Parámetro inválido Revisa WHATSAPP_PHONE_NUMBER_ID (es el id, no el teléfono)
    131030 Tu número no está en la lista de destinatarios permitidos del número de prueba Agrégalo y verifícalo en WhatsApp › Configuración de la API › Para (4.5, paso 2)
    131026 El número no puede recibir el mensaje Revisa que ADMIN_WHATSAPP_NUMBER tenga el indicativo (57…) y que ese número tenga WhatsApp
    132001 La plantilla no existe en ese idioma o aún no está aprobada Revisa en el Administrador de WhatsApp que koptup_aviso esté Activa en Spanish (es), igual que WHATSAPP_TEMPLATE_NAME y WHATSAPP_TEMPLATE_LANG
    132000, 132012 La cantidad o el formato de las variables no coincide El cuerpo debe tener exactamente {{1}} y {{2}} (4.5, paso 3)
    132015, 132016 La plantilla está pausada o deshabilitada Revísala en el Administrador de WhatsApp o crea una nueva con el mismo texto
    132018 Una variable trae saltos de línea, tabs o más de 4 espacios seguidos El backend ya los limpia (también los espacios Unicode como el de no separación); si aparece, revisa que el despliegue tenga el código actual
    133010 El número que envía no está registrado en la Cloud API Usa el número de prueba de Configuración de la API o registra el tuyo
    131047 Pasaron más de 24 h y se intentó enviar sin plantilla No debería pasar: el backend siempre usa la plantilla. Revisa que el despliegue tenga el código actual
  3. «Enviado» con respaldo hello_world: tu plantilla falló por un error de plantilla y, para que al menos sonara el teléfono, se envió la de ejemplo de Meta (en inglés, sin los datos del contacto). El aviso cuenta como enviado y no se reintenta: arregla la plantilla con el código que muestra el panel. La prueba mensual automática no lo da por bueno: si solo sale el respaldo, te llega además un correo «Falla en tus avisos por WhatsApp» con el error de la plantilla (y, al revés, un WhatsApp si el correo solo salió desde onboarding@resend.dev).

  4. «Enviado» pero no te llegó: sin webhook, «Enviado» solo significa que Meta aceptó el mensaje. Configura el webhook (4.5, paso 6): el contacto pasará a «Entregado», «Leído» o «Falló» con el código, y si falla te llega un correo de alerta.

  5. Después de meses sin usarla, Meta archiva una plantilla que pasa 12 meses sin envíos. La prueba mensual automática (NOTIFY_HEARTBEAT_DAYS, 30 por defecto) la mantiene activa: no la apagues.

  6. Versión de la API: WHATSAPP_GRAPH_VERSION usa v25.0. Meta retira cada versión unos 2 años después de publicarla; antes de que venza, súbela en Railway y prueba con el botón.

Mientras tanto, el contacto está guardado en Admin › Contactos y el aviso por correo sale aparte. Cuando arregles la causa, pulsa Reenviar aviso en el contacto (solo admin) o espera el reintento automático (cada 5 minutos, hasta 6 intentos en los primeros 3 días).


10. Acciones pendientes del dueño

Configuraciones que viven en las plataformas y no se pueden hacer desde el código. Ninguna cambia el comportamiento del sitio para los visitantes; todas reducen el riesgo de una caída o de un despliegue roto.

# Qué hacer Dónde Por qué
1 Proteger main con CI obligatorio: exigir pull request y que pasen los checks «Lint y tipos», «Pruebas unitarias (backend)», «Pruebas unitarias (web)», «Build», «E2E (Playwright)» y «Seguridad (dependencias y secretos)» GitHub › Settings › Rules › Rulesets (o Branches › Branch protection rules) sobre main Hoy un merge con CI en rojo llega a producción
2 Que Railway espere a CI: activar la opción «Wait for CI» (esperar los checks de GitHub antes de desplegar) Railway › servicio del backend › Settings (sección del repositorio conectado) Railway despliega cada push a main sin mirar CI
3 Dejar un solo proyecto de Vercel: conservar koptup (el que tiene www.koptup.com y koptup.com), copiar a él cualquier variable que solo exista en koptup-web y desconectar o borrar koptup-web Vercel › koptup-web › Settings › Git › Disconnect (o Settings › General › Delete Project) Los dos construyen cada push a main: doble build y riesgo de cambiar una variable en el proyecto equivocado
4 Montar un volumen para los archivos subidos en la ruta /app/uploads del servicio del backend Railway › servicio del backend › Volumes › New volume (mount path /app/uploads) Sin volumen, los adjuntos de pedidos y los archivos de cuentas médicas se borran en cada despliegue. Con volumen, un redespliegue puede dejar la API unos segundos sin servicio
5 Activar el monitoreo: crear la variable BACKEND_HEALTH_URL con <URL pública de la API>/health y revisar que lleguen los correos de Actions GitHub › Settings › Secrets and variables › Actions › Variables; tu perfil › Settings › Notifications Sin la variable, el monitoreo solo revisa la web (7.1)
6 Configurar los avisos al dueño (Resend, plantilla y token de Meta, variables en Railway, reenvío de ronald@koptup.com, DMARC, filtro de Gmail, Serverless apagado) y definir ADMIN_EMAIL e INTERNAL_API_KEY (la última también en Vercel) Paso a paso en 4.5 Sin ellas no llegan los avisos de nuevos contactos ni los correos a los prospectos, y los cupos por IP de las peticiones de la web cuentan con la IP de Vercel. El arranque avisa cuáles faltan y Admin › Configuración lo muestra
7 Revisar las alertas de dependencias antes del 31 de diciembre de 2026 (fecha de .github/audit-allowlist.json): migrar la web a una versión mayor de Next o volver a revisarlas Repositorio Cuando vence la fecha, el job Seguridad falla hasta que alguien las revise
8 Borrar NEXT_PUBLIC_SITE_URL del proyecto de Vercel si existe (opcional) Vercel › Settings › Environment Variables El código no la lee: evita pensar que cambia el dominio

La rotación de las credenciales que estuvieron en el historial del repositorio y el redespliegue del backend siguen en Estado y próximos pasos y en el runbook 9.2.


11. Limitaciones conocidas

# Qué pasa Efecto
1 El backend de producción no responde («Application not found» de Railway) Todo lo que necesita la API falla en www.koptup.com (9.1)
2 CI no protege main hasta que el dueño active la protección (acción 1 y 2) Un merge con pruebas en rojo llega a producción
3 Dos proyectos de Vercel construyen el mismo repositorio (koptup, con los dominios, y koptup-web) hasta que el dueño deje uno (acción 3) Doble compilación y riesgo de cambiar una variable en el proyecto equivocado: el que sirve www.koptup.com es koptup
4 Archivos subidos en disco: sin el volumen de Railway (acción 4), los adjuntos de pedidos y los archivos de cuentas médicas se borran en cada despliegue; los logs/ del disco también (usa los registros de Railway) Un archivo subido puede desaparecer después de un despliegue
5 Una sola instancia de la API (numReplicas: 1 en railway.json): los documentos de «Prueba con tu documento», los límites generales por IP y las cachés viven en la memoria del proceso Para correr más de una instancia hay que pasar ese estado a Redis primero; mientras tanto, no subas las réplicas en Railway
6 El monitoreo depende de GitHub Actions: corre cada 15 minutos (GitHub puede atrasarlo), solo desde main, y se pausa si el repositorio pasa 60 días sin actividad Una caída puede tardar hasta unos 20 minutos en avisarse (7.1)
7 Sin forma de restablecer la contraseña de otra cuenta desde el panel, y sin correo configurado no llega el enlace de recuperación Recuperar el acceso exige crear otra cuenta y darle el rol (9.4)
8 En la pantalla de una demo desactivada las dos etiquetas dicen «En mantenimiento» (la del modo y la del estado) Detalle visual repetido
9 Sin el webhook de WhatsApp, «Enviado» solo significa que Meta aceptó el mensaje, no que te llegó (4.5, paso 6) Un WhatsApp rechazado después por Meta no se ve en el panel ni se reintenta
10 Un aviso de WhatsApp que salió con el respaldo hello_world cuenta como enviado y no se reintenta El teléfono suena con un mensaje genérico en inglés; el panel muestra el error de la plantilla para que la arregles
11 La alerta por correo de un WhatsApp fallido se recuerda en memoria (los últimos 200 mensajes) Si el backend se reinicia y Meta repite el evento, la alerta puede llegar dos veces
12 Con «Serverless» encendido en Railway, el servicio se duerme sin tráfico Los avisos de un contacto nuevo salen igual (el formulario lo despierta), pero los reintentos y la prueba mensual no corren mientras duerme
13 El número de prueba de Meta solo escribe a números verificados en su lista (máximo 5) Para avisar a otro número hay que agregarlo y verificarlo, o registrar un número propio (con método de pago)

12. Para desarrolladores: archivos clave

Tema Archivo
CI .github/workflows/ci.yml
Seguridad (CI) scripts/audit-check.mjs, .github/audit-allowlist.json, .gitleaks.toml
Monitoreo .github/workflows/monitor.yml, scripts/uptime-check.sh
Publicación de la wiki .github/workflows/wiki-sync.yml
Despliegue de la API apps/backend/railway.json, apps/backend/Procfile, apps/backend/package.json (build, start, engines)
Despliegue de la web apps/web/vercel.json, apps/web/.env.production, apps/web/next.config.js (CSP y cabeceras)
Validación de variables apps/backend/src/config/env.ts
Arranque apps/backend/src/index.ts (orden de arranque, semilla del catálogo, jobs) y apps/backend/src/services/admin-bootstrap.service.ts (regla de ADMIN_EMAIL)
Salud y formato de error apps/backend/src/app.ts (/health/live, /health, CORS) y apps/backend/src/utils/http-error.ts ({ success: false, code, message })
Desarrollo local docker-compose.dev.yml (MongoDB y Redis), SETUP.md
URL del backend en la web apps/web/src/lib/backend-url.ts
Topes de IA apps/backend/src/services/ai-budget.service.ts
«Prueba con tu documento» apps/backend/src/services/demo-rag.service.ts, apps/backend/src/routes/demo-rag.routes.ts
Chatbot en MongoDB apps/backend/src/models/ChatbotBot.ts, apps/backend/src/services/chatbot-store.service.ts
Privacidad y conservación de datos apps/backend/src/services/privacy.service.ts, apps/backend/src/services/retention.service.ts, apps/backend/src/jobs/retention.job.ts
Correo y WhatsApp (avisos al dueño) apps/backend/src/services/email.service.ts (Resend y SMTP), apps/backend/src/services/whatsapp.service.ts (plantilla de Meta), apps/backend/src/services/owner-notify.service.ts (arma el aviso, lo envía y guarda el resultado), apps/backend/src/jobs/notify-retry.job.ts, apps/backend/src/jobs/notify-heartbeat.job.ts, apps/backend/src/routes/admin-notifications.routes.ts, apps/backend/src/routes/whatsapp-webhook.routes.ts; en la web, apps/web/src/components/admin/OwnerNotificationsCard.tsx y owner-notify.tsx
Dar el rol admin apps/backend/src/scripts/set-admin.ts, apps/backend/ADMIN_SETUP.md
Plantillas de variables apps/backend/.env.example, apps/web/.env.example
Pruebas apps/backend/jest.config.js, apps/web/jest.config.js, apps/web/playwright.config.ts, apps/web/e2e/
Mocks de OpenAI y de Resend + WhatsApp apps/web/e2e/support/openai-mock.js, apps/web/e2e/support/notify-mock.js

13. Páginas relacionadas

Inicio · Estado y próximos pasos · Correcciones pendientes

Documentación (lo que existe hoy)

Estrategia

Cliente y demos

Sitio

Productos

Plataforma

Roadmap

Clone this wiki locally