Repository navigation
Doc 11 Operacion y Despliegue
Resumen. El código vive en GitHub. Cada push a
mainlo 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.
- El despliegue en una imagen
- Dónde corre cada pieza
- Integración continua (CI)
- Variables de entorno (y 4.5 Configurar los avisos al dueño)
- Levantar todo en local, paso a paso
- Pruebas: unitarias, de integración y de punta a punta
- Salud del sistema, monitoreo y registros
- Cómo se publica esta wiki
- Runbooks: qué hacer cuando algo falla
- Acciones pendientes del dueño
- Limitaciones conocidas
- Para desarrolladores: archivos clave
- Páginas relacionadas
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
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:
-
CI todavía no es una puerta. Vercel y Railway despliegan
mainsin esperar a que CI termine, ymainno exige CI en verde para fusionar: una prueba en rojo no frena el despliegue hasta que el dueño active la protección demain(acciones pendientes). - La web y la API se despliegan por separado. Puedes revertir una sin tocar la otra (runbook 9.9).
-
Las variables
NEXT_PUBLIC_*se fijan al compilar la web. Cambiarlas en Vercel no tiene efecto hasta que vuelvas a desplegar.
| 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.
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
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
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.
-
Web (Vercel): Project › Settings › Environment Variables del proyecto
koptup. LasNEXT_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 faltaMONGODB_URIoJWT_SECRETy escribeConfiguració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.exampleyapps/web/.env.examplesolo 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
En rojo, lo que el visitante o el dueño notan; en morado, lo que tiene una alternativa.
| 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).
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 |
| 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 |
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
Los dos canales son independientes: si uno falla, el otro sale igual. El resultado de cada uno queda en el contacto.
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.
- 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).
-
API Keys › Create API Key: nombre
koptup-railway, permiso Sending access, dominiokoptup.com. Copia la key: empieza porre_y se muestra una sola vez. - El plan Free alcanza de sobra: 3.000 correos al mes y 100 al día, y la key no vence.
- 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).
- 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. - 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). - 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.
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.
-
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.
-
Categoría Utility (Utilidad), nombre
koptup_aviso, idioma Spanish (es). -
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. -
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. -
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.
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:
- En business.facebook.com › Configuración (del negocio) › Usuarios › Usuarios del sistema › Agregar: nombre
koptup-avisos, rol Administrador. - Asignar activos: la app del paso 2 y la cuenta de WhatsApp del número, las dos con control total.
-
Generar nuevo token: elige la app, caducidad Nunca y los permisos
whatsapp_business_messagingywhatsapp_business_management. Copia el token (se muestra una sola vez): va enWHATSAPP_API_TOKEN.
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.
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.
- 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. - Inventa un texto largo al azar para la verificación, por ejemplo con
node -e "console.log(require('crypto').randomBytes(24).toString('hex'))": va enWHATSAPP_WEBHOOK_VERIFY_TOKEN. - En la app de Meta › Configuración de la app › Básica › Clave secreta de la app (Mostrar): va en
WHATSAPP_APP_SECRET. - Define las dos en Railway y despliega antes del paso siguiente: sin ellas la ruta responde 404 y Meta no la puede verificar.
- En la app › WhatsApp › Configuración › Webhook › Editar: URL de devolución de llamada
https://<dominio de la API>/api/webhooks/whatsappy, como token de verificación, el mismo texto del punto 1. Verificar y guardar. - 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.
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.
- Namecheap › Domain List ›
koptup.com› Manage › Redirect Email (Email Forwarding) › Add Forwarder: aliasronald→ 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. -
Advanced DNS › Add New Record: tipo TXT Record, host
_dmarc, valorv=DMARC1; p=none;. Ayuda a que Gmail confíe en el correo que sale dekoptup.comsin bloquear nada. - No toques los registros
resend._domainkeynisend. - Prueba: escribe desde otra cuenta a
ronald@koptup.com; en unos minutos debe llegar a tu Gmail.
- En Gmail, en la barra de búsqueda › Mostrar opciones de búsqueda › De:
avisos@koptup.com OR onboarding@resend.dev. - Crear filtro › marca Nunca enviarlo a Spam (y, si quieres, Marcar siempre como importante) › Crear filtro.
- 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.
- 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). - 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.
| 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) | — |
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
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).
-
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 -
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 en127.0.0.1;npm run docker:downlos 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. -
(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 -
Configura el backend: copia la plantilla y ajusta lo mínimo.
cp apps/backend/.env.example apps/backend/.env
En
apps/backend/.env: dejaMONGODB_URI=mongodb://localhost:27017/koptup_dbyREDIS_URL=redis://localhost:6379; cambiaJWT_SECRETyJWT_REFRESH_SECRETpor dos valores largos y distintos; para la IA, pon tuOPENAI_API_KEYo, con el mock,OPENAI_BASE_URL=http://127.0.0.1:3999/v1y 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 apuntaRESEND_API_URLyWHATSAPP_GRAPH_URLahttp://127.0.0.1:3998con valores ficticios; la lista completa está enSETUP.md(sección 4). Lo que el backend «envió» se ve enhttp://127.0.0.1:3998/__requests. -
Arranca el backend:
cd apps/backend && npm run dev # http://localhost:3001
Comprueba
http://localhost:3001/health(debe decirhealthyymongo: connected). En local tienes además la documentación interactiva enhttp://localhost:3001/api-docs. -
Configura la web: crea
apps/web/.env.localcon 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 ahttp://localhost:3001. Sin el archivo,next devusa igualhttp://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á enapps/web/.env.production, que Next carga ennext buildynext start. -
Arranca la web:
cd apps/web && npm run dev # http://localhost:3000
El script
predevregenera los mensajes agregados de las demos antes de arrancar. -
Crea tu cuenta admin: regístrate en
http://localhost:3000/registery dale el rol de una de estas dos formas:-
corre el script desde
apps/backend(leeapps/backend/.env; unaMONGODB_URIescrita 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.coenapps/backend/.envy 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_changecon 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: 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.

Con el mock, la IA es simulada pero el recorrido es real: subida, fragmentos, búsqueda y cita.
- Respuesta simulada: el mock repite los fragmentos que recibió; con una clave real aquí aparece la respuesta del modelo.
- Fuentes: el fragmento citado con su página, igual que en producción.
- 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).
| 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):
- 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). - Backend compilado:
npm run build --workspace=apps/backendy luego, enapps/backend,node dist/index.jsconPORT=3001 NODE_ENV=production, una base propia (koptup_e2e), los dos JWT de prueba,CORS_ORIGINyFRONTEND_URLenhttp://localhost:3000, el mock enOPENAI_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_URLyWHATSAPP_GRAPH_URLenhttp://127.0.0.1:3998, másNOTIFY_JOBS_ENABLED=false; los valores exactos están en elREADME.md). Nunca con las credenciales reales: cada contacto de prueba avisaría de verdad. - Web compilada:
NEXT_PUBLIC_API_URL=http://localhost:3001 npm run build --workspace=apps/webynpx next start -p 3000enapps/web. -
npx playwright install chromium(la primera vez) yE2E_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.
| 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 |

Respuesta real de GET /api/demo-rag/status en el entorno local.
-
enabled: si la función está encendida. -
reason: por qué no lo está (disabled,unavailableobudget_exhausted);nullsi está encendida. -
budgetExhausted: si se alcanzó el tope del mes (nunca muestra montos). - 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.
El workflow .github/workflows/monitor.yml corre cada 15 minutos (y a mano desde la pestaña Actions) el script scripts/uptime-check.sh:
-
GET https://www.koptup.comdebe responder 2xx o 3xx (sigue redirecciones). -
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. - 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
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_URLen 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 enmain.
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.
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
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.mdal publicar. Las imágenes van endocs/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á enmain. 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 desdemainvuelve a dejarla igual amain.
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
Busca el síntoma a la izquierda y ve al runbook de la derecha.
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
-
Confirma el síntoma. Abre en el navegador
<URL de la API>/health/live. La URL es el valor deNEXT_PUBLIC_API_URLen Vercel o, si no está, el deapps/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.
-
- 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)?
-
Si el servicio no existe, créalo desde GitHub:
-
New service › GitHub repo con este repositorio y la rama
main. - Directorio raíz
apps/backend(ahí estárailway.json, que define build, arranque y chequeo de salud). - Agrega un Redis al proyecto y copia su URL en
REDIS_URL. - 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_EMAILy las de los avisos al dueño (4.5). Aprovecha para usar secretos nuevos (9.2) y deja apagado Serverless en Settings. - Genera un dominio público para el servicio.
- Monta un volumen en
/app/uploadspara que los archivos subidos no se borren en cada despliegue, y pon<dominio>/healthen la variableBACKEND_HEALTH_URLdel repositorio (acciones pendientes).
-
New service › GitHub repo con este repositorio y la rama
-
Si el dominio de la API cambió: en Vercel (proyecto
koptup, entorno Production) pon la nueva URL enNEXT_PUBLIC_API_URLy redespliega la web (Deployments › ⋯ › Redeploy). La misma variable alimenta la CSP, así que no hay que tocar código. Después actualiza tambiénapps/web/.env.productionen el repositorio para que no quede desalineado. -
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).
-
-
Espera el chequeo de Railway: el despliegue entra en servicio cuando
/healthresponde 200 (con MongoDB conectado) antes de 120 segundos. Si no lo logra, Railway marca el despliegue como fallido y deja el anterior; revisaMONGODB_URIy que la base acepte conexiones desde Railway. -
Verifica de afuera hacia adentro:
-
GET /health→200conmongo: connected. Si da503, revisaMONGODB_URIy que la base acepte conexiones desde Railway. -
GET /api/demo-catalog→ lista de 28 demos. - 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/erpsin sesión: debe mostrar «Solicitar acceso», no «No se pudo verificar». - Si el navegador muestra errores de CORS, revisa que la web use
https://www.koptup.como agrega su dominio enCORS_ORIGIN.
-

Lo que ve hoy un visitante en cualquier demo «Con solicitud» de www.koptup.com (reproducido en local).
- Estado de la pantalla de acceso: «No se pudo verificar».
- Mensaje: el servicio no respondió.
- Intentar de nuevo: vuelve a cargar la demo.
- Solicitar acceso: lleva al formulario, que también necesita la API.
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
- Cambia primero en el proveedor (por ejemplo, la contraseña del usuario de MongoDB o una clave nueva de OpenAI).
- Actualiza la variable en Railway (y en Vercel si es
INTERNAL_API_KEY). Usa valores distintos para los dos JWT. - Redespliega (Railway lo propone al guardar; Vercel exige Redeploy).
-
Verifica:
GET /healthen200, inicio de sesión en/admin, Enviar aviso de prueba en Admin › Configuración (correo y WhatsApp) y una pregunta en/demo/chatbot. - 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).
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
- Consulta
GET <URL de la API>/api/demo-rag/statusy leereason. - En Railway, define lo que falte:
DEMO_UPLOAD_ENABLED=true,REDIS_URL(Redis del proyecto) yOPENAI_API_KEY. Recomendadas:TRUST_PROXY_HOPS=1(cupo de 3 documentos por IP al día),MONGODB_URI(guarda el email como lead con origendemo-rag) y, si quieres,DEMO_MONTHLY_BUDGET_USD. - Redespliega la API.
-
Verifica:
statusdebe responder"enabled": truey"reason": null(captura de la sección 7). Luego sube un PDF corto en/demo/chatbot?mode=uploady haz una pregunta.
Para apagarla, pon DEMO_UPLOAD_ENABLED=false (o bórrala) y redespliega. La demo con los documentos de ejemplo sigue funcionando.
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
- Registra la cuenta en
https://www.koptup.com/register(o usa una que ya exista). - Pon su email en
ADMIN_EMAILen 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 scriptset-admino el panel. - Cierra sesión y vuelve a entrar: el panel decide qué mostrar con el rol guardado al iniciar sesión.
Olvidaste la contraseña
- Usa «¿Olvidaste tu contraseña?» en
/login. Llega un enlace que vale 1 hora y sirve una sola vez. Necesita correo configurado (9.7). - 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_EMAILno sirve aquí, porque la cuenta vieja sigue siendo admin). Desde ella puedes seguir operando. - Las cuentas creadas con Google no tienen contraseña: entra con «Continuar con Google» (requiere
GOOGLE_CLIENT_IDyGOOGLE_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.
Cuándo. Quieres que una demo sea abierta, que pida acceso o que sea solo por invitación, o apagarla («En mantenimiento»).
- 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). - En la fila de la demo, elige el modo (Abierta, Requiere acceso o Solo por invitación) o apaga Activa.
- Pulsa Guardar en esa fila. El cambio queda en la bitácora con tu usuario.
- Espera hasta un minuto: la web guarda el catálogo en memoria 60 segundos.
-
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.
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
-
Confirma que es el cupo y no otra cosa. Para la demo con documento,
GET /api/demo-rag/statusdicebudget_exhausted. Si el mensaje habla de «no disponible», revisa primeroOPENAI_API_KEYyREDIS_URL: sin Redis no se puede medir el gasto y la IA se apaga. - 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.
-
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).
- 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.
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
Pasos
-
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_KEYyNOTIFY_EMAIL(oADMIN_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.
- «No configurado» con la lista de lo que falta → define
-
Pulsa «Enviar aviso de prueba» (solo admin) y lee el resultado del correo:
-
Resend 401oResend 403(key inválida, revocada o sin permiso) → crea una key nueva con Sending access y cámbiala en Railway. -
403que menciona el dominio o la verificación →koptup.comno está verificado en esa cuenta de Resend. El aviso al dueño se reintenta solo desdeonboarding@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 registrosresend._domainkeynisendde Namecheap. -
422(validación) → revisa el formato deEMAIL_FROM(Nombre <correo@koptup.com>) y deNOTIFY_EMAIL. -
429o5xxo «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ó.
-
- 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.
-
En los registros de Railway busca
Correo "(cada envío diceenviado por resend a ro***@…ofalló por resend: …) y los avisos[env] RESEND_API_KEY …del arranque. -
Revisa
FRONTEND_URL: si un correo llega con un enlace alocalhost, faltahttps://www.koptup.com. -
Recordatorios de vencimiento: además del correo, el job debe estar encendido (
DEMO_GRANTS_JOB_ENABLEDdistinto defalse); 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.
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.

Una demo desactivada desde el catálogo.
- Estado «En mantenimiento».
- Ver otras demos: vuelve al catálogo.
- Solicitar demo guiada: abre el formulario con esa demo elegida.

La misma pantalla en un celular (390 × 844).
Cuándo. Un despliegue nuevo rompió algo y necesitas volver atrás ya.
Web (Vercel), en minutos
- Abre el proyecto
koptupen Vercel › Deployments y filtra por Production. - Ubica el último despliegue de producción que funcionaba (cada uno muestra el commit y el mensaje del merge).
- En su menú elige Instant Rollback (o Promote to Production). El dominio pasa a ese despliegue sin volver a compilar.
-
Verifica
www.koptup.comy la página que fallaba. -
Importante: el siguiente push a
mainvuelve a desplegar el código con el error. Corrígelo enmain(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)
- En el servicio del backend › Deployments, elige el despliegue anterior y usa Redeploy (o Rollback).
- Espera el chequeo
/healthde Railway y verificaGET /healthdesde el navegador.
Corregir main (definitivo)
- Crea una rama y ejecuta
git revert -m 1 <commit del merge>. - Abre un PR, espera CI en verde y fusiona. Vercel y Railway despliegan el código revertido.
- 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.
Síntoma. Llega un correo de GitHub «Run failed: Monitoreo» o la pestaña Actions muestra la corrida en rojo.
-
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;503en/health= API sin base de datos;404con «Application not found» = Railway no tiene servicio en ese dominio). - 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.
-
Si la web no responde: revisa el estado del despliegue en Vercel (proyecto
koptup) y, si el último despliegue la rompió, revierte. - Si la API no responde o da 503: sigue el runbook 9.1 desde el paso 1.
- Cuando vuelva, la siguiente corrida programada queda en verde; GitHub no envía correo de recuperación.
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
Pasos
-
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_IDoADMIN_WHATSAPP_NUMBER; oWHATSAPP_PROVIDERsi pide un proveedor retirado). El registro del arranque dice lo mismo en la líneaAvisos por WhatsApp: …. -
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 190El 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 10o200La app no tiene el permiso Al generar el token, marca whatsapp_business_messagingywhatsapp_business_management, y asigna la app y la cuenta de WhatsApp al usuario del sistema100Parámetro inválido Revisa WHATSAPP_PHONE_NUMBER_ID(es el id, no el teléfono)131030Tu 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) 131026El número no puede recibir el mensaje Revisa que ADMIN_WHATSAPP_NUMBERtenga el indicativo (57…) y que ese número tenga WhatsApp132001La plantilla no existe en ese idioma o aún no está aprobada Revisa en el Administrador de WhatsApp que koptup_avisoesté Activa en Spanish (es), igual queWHATSAPP_TEMPLATE_NAMEyWHATSAPP_TEMPLATE_LANG132000,132012La cantidad o el formato de las variables no coincide El cuerpo debe tener exactamente {{1}}y{{2}}(4.5, paso 3)132015,132016La plantilla está pausada o deshabilitada Revísala en el Administrador de WhatsApp o crea una nueva con el mismo texto 132018Una 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 133010El 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 131047Pasaron 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 -
«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ó desdeonboarding@resend.dev). -
«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.
-
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. -
Versión de la API:
WHATSAPP_GRAPH_VERSIONusav25.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).
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.
| # | 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) |
| 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
|
- Arquitectura: las piezas y cómo se hablan.
- Flujos de administración y operación: diagramas de despliegue, publicación de la wiki y runbooks.
- Flujos técnicos: validación de variables, arranque y salud.
- Manual del administrador: las acciones del panel con capturas.
- SEO, analítica y legal: variables de medición y datos personales.
- API · Roles y permisos · Glosario y preguntas
- Estado y próximos pasos: acciones pendientes del dueño. Plan original: Backend y API y Seguridad y calidad.
Inicio · Estado y próximos pasos · Correcciones pendientes
Documentación (lo que existe hoy)
- 00 · Índice
- 01 · Arquitectura · 02 · Mapa del sitio
- 03 · Páginas públicas
- 04 · Flujos del visitante
- 05 · Prospecto y cliente
- 06 · Manual del administrador
- 07 · Guía de demos (28)
- 08 · API · 09 · Modelos de datos
- 10 · Roles y permisos
- 11 · Operación y despliegue
- 12 · SEO, analítica y legal
- 13 · Glosario y preguntas
- 14 · Diagramas de flujo: negocio · administración · técnicos
Estrategia
Cliente y demos
Sitio
Productos
- 08 · Catálogo de productos
- Producto principal: Sistemas RAG
- Otras soluciones a medida:
- Ventas y atención: CRM · Helpdesk · Voice AI · Fidelización
- Finanzas y talento: ERP · Facturación · HRMS · Firma
- Comercio y logística: E-commerce · POS · WMS · Delivery · Reservas
- Datos y productividad: BI · Documentos · Automatización · Extracción de datos · CMS · Proyectos
- Salud y educación: Telemedicina · LMS
- Tecnología: Code review · QA · SaaS multi-tenant · VPN · Moderación
- Fuera del catálogo: Cuentas médicas · Motor de reglas · LinkedIn Ads
Plataforma
Roadmap