Portal Digincrease — Documentación técnica

Referencia completa del sistema: arquitectura, base de datos, APIs, webhooks y automatización.

Visión general

El Portal de Digincrease es una plataforma SaaS multi-tenant para agencias de marketing. Permite gestionar clientes, contenido, facturación, inbox omnicanal y automatizaciones de respuesta con IA.

Framework

Next.js 14.2 (App Router)

Base de datos

PostgreSQL + Prisma v5

Infraestructura

VPS Ubuntu + Docker + Caddy

Auth

NextAuth.js v4 (JWT)

Mensajería

Meta Cloud API v25.0

Automatización

n8n self-hosted

Arquitectura

El sistema corre en un único VPS con Docker Compose. Caddy actúa como reverse proxy con SSL automático.


  ┌─────────────────────────────────────────────────────┐
  │                   VPS (tu-servidor-ip)               │
  │                                                     │
  │  Caddy (443/80)                                     │
  │   ├── portal.tudominio.com  → :3005 (Next.js)       │
  │   ├── docs.tudominio.com    → :3005/docs             │
  │   └── automation.tudominio.com → :5678 (n8n)        │
  │                                                     │
  │  Docker Compose                                     │
  │   ├── web  (node:20 / Next.js)  → puerto 3005       │
  │   └── postgres (PostgreSQL 15)  → interno           │
  └─────────────────────────────────────────────────────┘

  Flujo de mensaje entrante (WhatsApp):
  ┌──────────┐    ┌──────────────┐    ┌──────────┐
  │ Usuario  │───▶│  Meta Cloud  │───▶│  Portal  │
  │ WhatsApp │    │  API Webhook │    │/api/     │
  └──────────┘    └──────────────┘    │webhooks/ │
                                      │meta      │
                                      └────┬─────┘
                                           │ guarda en DB
                                           │ reenvía a n8n
                                      ┌────▼─────┐
                                      │   n8n    │
                                      │ AI Agent │
                                      └────┬─────┘
                                           │ POST /api/v1/...
                                      ┌────▼─────┐
                                      │  Portal  │
                                      │ API send │
                                      └────┬─────┘
                                           │
                                      ┌────▼──────┐
                                      │  Meta API │
                                      │ /messages │
                                      └───────────┘

Base de datos

PostgreSQL gestionado con Prisma ORM. El schema completo está en prisma/schema.prisma. Los comandos de migración se ejecutan con prisma db push dentro de un container efímero node:20-alpine.

Agency

Tabla raíz del sistema. Cada agencia tiene sus propios clientes, configuración de canales y datos de facturación.

CampoTipoDescripción
idString (cuid)PK — identificador único
nameStringNombre de la agencia
emailString?Email de contacto
channelConfigJson?Config de canales: { whatsapp: { phoneNumberId, businessAccountId } }
createdAtDateTimeFecha de creación

Client

Clientes que pertenecen a una agencia. Cada cliente tiene su propio portal de acceso.

CampoTipoDescripción
idString (cuid)PK
agencyIdStringFK → Agency
companyNameStringNombre de la empresa
emailString?Email del cliente
planTypeStringbasic | pro | enterprise
planStatusStringactive | overdue | cancelled
nextBillingDateDateTime?Próximo vencimiento
amountDueFloat?Monto a cobrar

User

CampoTipoDescripción
idStringPK
emailString (único)Email para login
roleStringadmin | client
agencyIdString?FK → Agency (solo admins)
clientIdString?FK → Client (solo clientes)
passwordHashStringbcrypt hash

InboxConversation

CampoTipoDescripción
idString (cuid)PK — también usado como conversation_id en n8n
agencyIdStringFK → Agency
clientIdString?FK → Client (opcional, para inbox por cliente)
channelStringwhatsapp | instagram | messenger | email
contactIdStringFK → InboxContact
statusStringopen | closed
lastMessageAtDateTime?Timestamp del último mensaje

InboxContact

CampoTipoDescripción
idStringPK
agencyIdStringFK → Agency
channelStringCanal del contacto
externalIdStringWA ID, PSID, IG ID según canal
nameString?Nombre del contacto
phoneString?Teléfono con + (ej: +54911XXXXXXXX)
avatarUrlString?URL de foto de perfil

InboxMessage

CampoTipoDescripción
idStringPK — puede ser el mid de Meta para evitar duplicados
conversationIdStringFK → InboxConversation
directionStringinbound | outbound
bodyStringTexto del mensaje
mediaUrlString?URL de imagen/video/audio
mediaTypeString?image | audio | video | document
externalIdString?ID original de Meta
readAtDateTime?Null = no leído (solo inbound)
createdAtDateTimeTimestamp del mensaje

SocialAccount

CampoTipoDescripción
idStringPK
clientIdStringFK → Client
platformStringfacebook | instagram | whatsapp
accountIdStringPage ID / IG User ID / Phone Number ID
pageIdString?Page ID de Facebook (para Messenger)
igUserIdString?IG User ID (para Instagram)
accessTokenString?Token de acceso (page token o user token)
isActiveBooleanSi la cuenta está activa

Diagrama de base de datos

Estructura visual de las tablas y sus relaciones. Las flechas indican claves foráneas (FK). Los campos con ? son opcionales.

1:N1:N1:N0:N1:N1:N1:N1:NAgencyid name channelConfig?ClientidagencyId companyName planType planStatusUserid email roleagencyId?clientId?InboxConversationidagencyIdclientId? channelcontactId statusSocialAccountidclientId platform accountId isActiveInboxContactidagencyId channel externalId name? phone?InboxMessageidconversationId direction body readAt? createdAtContentPieceidclientId title? status scheduledDate?◆ PK→ FKcampo? = opcional- - → relación opcional■ Agency/Auth■ Client■ Inbox■ Content

API Reference

Todas las rutas bajo /api/admin/* requieren sesión NextAuth con role: admin. Las rutas /api/portal/* requieren role: client.

Inbox — Admin

GET
/api/admin/inbox?channel=whatsapp

Lista conversaciones de la agencia filtradas por canal (all | whatsapp | instagram | messenger | email)

GET
/api/admin/inbox?conversationId=...

Mensajes de una conversación. Marca mensajes inbound como leídos.

POST
/api/admin/inbox

Envía un mensaje outbound. Body: { conversationId, body }. Envía por Meta API y guarda en DB.

PATCH
/api/admin/inbox

Abre o cierra una conversación. Body: { conversationId, status: 'open' | 'closed' }

Clientes — Admin

GET
/api/admin/clientes

Lista todos los clientes de la agencia (id, companyName, email, planType, planStatus)

POST
/api/admin/clientes

Crea un nuevo cliente con usuario asociado. Body: { companyName, email, password, planType, ... }

Configuración — Admin

GET
/api/admin/configuracion

Devuelve channelStatus (connected/disconnected por canal) y channelConfig

PATCH
/api/admin/configuracion

Actualiza channelConfig. Body: { channelConfig: { whatsapp: { phoneNumberId, businessAccountId } } }

Webhooks — Públicos

GET
/api/webhooks/meta

Verificación del webhook de Meta. Params: hub.mode, hub.verify_token, hub.challenge

POST
/api/webhooks/meta

Recibe mensajes de Meta (WhatsApp, Messenger, Instagram). Guarda en DB y reenvía a n8n.

Automatización — API compatible con Chatwoot

POST
/api/v1/accounts/[agencyId]/conversations/[conversationId]/messages

Endpoint que usa n8n para enviar respuestas. Header: api_access_token. Body: { content, message_type, content_type }

Portal — Client

GET
/api/portal/inbox

Conversaciones del cliente autenticado (ownership verificada por clientId en sesión)

Meta Webhooks

Configuración en Meta Developer

La misma URL y token se usa para todos los canales (WhatsApp, Messenger, Instagram).

CampoValor
URL del Callbackhttps://portal.tudominio.com/api/webhooks/meta
Verify Tokentu_verify_token_secreto (definido en META_WEBHOOK_VERIFY_TOKEN)
Campos suscriptosmessages (WhatsApp), messages (Messenger), messages (Instagram)

Routing por objeto

POST /api/webhooks/meta
body.object === 'page'                    → Messenger handler
body.object === 'instagram'               → Instagram handler
body.object === 'whatsapp_business_account' → WhatsApp handler

Lookup de agencia para WhatsApp

WhatsApp no tiene OAuth como Instagram/Facebook, por lo que no existe SocialAccount. El handler busca la agencia por el phoneNumberId guardado en Agency.channelConfig:

// 1. Buscar en SocialAccount (por si hay cuenta vinculada)
// 2. Fallback: buscar agencia donde channelConfig.whatsapp.phoneNumberId === phoneNumberId
const match = agencies.find(a => a.channelConfig?.whatsapp?.phoneNumberId === phoneNumberId)

Automatización n8n

Flujo completo

1. WhatsApp → Meta Cloud → POST /api/webhooks/meta
2. Portal guarda mensaje en InboxMessage (direction: inbound)
3. Portal hace POST a AUTOMATION_WEBHOOK_URL con formato Chatwoot-compatible:
   {
     body: {
       message_type: "incoming",
       content: "texto del mensaje",
       content_type: "text",
       conversation: {
         id: "<conversationId>",               ← UUID del portal, no número
         meta: { sender: { phone_number: "+54911XXXXXXXX", name: "Nombre del contacto" } },
         labels: [],
         messages: [{ sender: { phone_number: "+54911XXXXXXXX" } }]
       },
       account: { id: "<agencyId>" }
     }
   }
4. n8n AI Agent procesa y llama al portal:
   POST /api/v1/accounts/{agencyId}/conversations/{conversationId}/messages
   Header: api_access_token: <PORTAL_API_TOKEN>
   Body: { content: "respuesta del bot", message_type: "outgoing" }
5. Portal envía via WhatsApp Cloud API y guarda en InboxMessage (direction: outbound)

Cambios en el nodo "Enviar a Chatwoot1"

CampoValor anterior (Chatwoot)Valor nuevo (Portal)
URLigual — no cambia (mismo dominio)igual — no cambia (mismo dominio)
api_access_token<token_chatwoot_anterior><PORTAL_API_TOKEN> definido en .env

El resto del flujo n8n (Redis buffer, AI Agent, Airtable, Google Calendar) no requiere cambios.

Variables relevantes en n8n

Expresión n8nValor que recibe del portal
$json.body.conversation.idUUID de InboxConversation (ej: cm9xxxxxxxxxxxxx)
$json.body.account.idUUID de Agency (ej: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
$json.body.conversation.meta.sender.phone_number+54911XXXXXXXX (con prefijo +)
$json.body.conversation.meta.sender.nameNombre del contacto de WhatsApp
$json.body.contentTexto del mensaje entrante
$json.body.conversation.labels[] (array vacío — bot siempre activo)

Sistema de Inbox

Arquitectura del inbox

El inbox es multi-canal y multi-tenant. Cada conversación pertenece a una agencia y opcionalmente a un cliente específico.

InboxContact (1) ──── (N) InboxConversation (1) ──── (N) InboxMessage
                              │
                              ├── agencyId  (siempre presente)
                              └── clientId  (opcional, para inbox por cliente)

Unread count

Los mensajes inbound tienen readAt: null hasta que el admin abre la conversación. El endpoint GET con conversationId los marca como leídos automáticamente.

Auto-refresh

El frontend hace polling cada 3 segundos mientras hay una conversación activa. El scroll automático solo se activa si el usuario está en el fondo del chat.

Envío optimista

Al enviar, el mensaje aparece inmediatamente con opacidad reducida (estado temporal) mientras se procesa el POST. Luego se reemplaza con el dato real del servidor.

Variables de entorno

Archivo .env en /home/ubuntu/marketing-portal/ en el VPS.

VariableEjemplo / Descripción
DATABASE_URLpostgresql://USER:PASSWORD@postgres:5432/DB_NAME
NEXTAUTH_SECRETstring aleatorio largo (ej: generado con openssl rand -base64 32)
NEXTAUTH_URLhttps://portal.tudominio.com
META_WEBHOOK_VERIFY_TOKENtoken que elegís vos y configurás en Meta Developer
META_WHATSAPP_PHONE_NUMBER_IDID del número de WhatsApp Business (Meta Developer → WhatsApp → API Setup)
META_WHATSAPP_BUSINESS_ACCOUNT_IDID de la cuenta WABA (Meta Developer → WhatsApp → API Setup)
META_WHATSAPP_ACCESS_TOKENToken de acceso de WhatsApp Cloud API (Meta Developer → WhatsApp → API Setup)
META_FACEBOOK_ACCESS_TOKENToken de página de Facebook (Meta Developer → Messenger → Settings)
META_INSTAGRAM_ACCESS_TOKENToken de acceso de Instagram (Meta Developer → Instagram → API Setup)
PORTAL_API_TOKENtoken secreto que compartís con n8n para autenticar respuestas del bot
AUTOMATION_WEBHOOK_URLhttps://automation.tudominio.com/webhook/tu-path

Comandos útiles en VPS

# Ver logs en tiempo real
ssh -i ~/tu-ssh-key.pem ubuntu@TU_VPS_IP \
  "docker logs -f nombre-del-container-web"

# Rebuild y deploy
cd /home/ubuntu/tu-proyecto
docker compose build --no-cache && docker compose up -d

# Aplicar cambios de schema Prisma
APP=/home/ubuntu/tu-proyecto
docker run --rm --network tu-proyecto_net \
  -v $APP/prisma:/app/prisma \
  -e DATABASE_URL='postgresql://USER:PASSWORD@postgres:5432/DB_NAME' \
  -w /app node:20-alpine sh -c \
  'apk add -q openssl && npm install -g prisma@5 && prisma db push --skip-generate'

# Consultar DB directamente
docker exec nombre-del-container-postgres psql -U postgres nombre_db -c "SELECT ..."

Portal Digincrease — Documentación técnica interna. Actualizado en Junio 2026.