node-api
Backend principal de Crimoo. Express 5 + TypeScript + Clean Architecture.
Stack
- Framework: Express.js 5.x + TypeScript
- BD Principal: PostgreSQL (TypeORM, 34 entities)
- Analytics: ClickHouse
- Mensajería: Google Cloud Pub/Sub
- Auth: JWT + Google OAuth 2.0
- Pagos: Stripe
- IA: Google Gemini
Entidades principales
Endpoints principales
Auth /api/auth
| Método | Path | Auth |
|---|---|---|
| POST | /login | No |
| POST | /register | No |
| GET | /google | No |
| GET | /google/callback | No |
| GET | /me | JWT |
| POST | /logout | No |
GTM /api/v1/gtm
| Método | Path | Descripción |
|---|---|---|
| POST | / | Crear GTM → publica a Pub/Sub |
| GET | / | Listar GTMs del workspace |
| GET | /:id | Obtener GTM |
| DELETE | /:id | Eliminar GTM |
| PUT | /:id/custom-loader | Habilitar custom loader |
| GET | /:id/logs | Logs históricos (ClickHouse) |
| GET | /:id/logs/stream | Stream SSE en vivo |
| GET | /:id/health | Health del container |
CRM /api/contacts + /api/deals
| Método | Path |
|---|---|
| GET/POST | /contacts |
| GET/PUT/DELETE | /contacts/:id |
| GET | /contacts/:id/timeline |
| POST | /contacts/:id/activities |
| GET/POST | /deals |
| PUT/DELETE | /deals/:id |
CRM Webhooks /api/crm/webhooks
| Método | Path | Descripción |
|---|---|---|
| GET | /:gtmId | Listar webhooks del container |
| POST | /:gtmId | Crear webhook (secret visible solo al crear) |
| GET | /:gtmId/:id | Obtener webhook (secret enmascarado) |
| PUT | /:gtmId/:id | Actualizar webhook |
| DELETE | /:gtmId/:id | Eliminar webhook |
| POST | /:gtmId/:id/test | Enviar entrega de prueba |
| GET | /:gtmId/:id/deliveries | Historial de entregas (TTL 7 días) |
| POST | /:gtmId/:id/reset-circuit | Resetear circuit breaker |
Conversiones /api/conversions
| Método | Path |
|---|---|
| GET/POST | /credentials |
| GET/PUT/DELETE | /credentials/:id |
| POST | /credentials/:id/test |
| GET/POST | /triggers |
| PATCH | /triggers/:id/toggle |
| GET | /events |
| GET | /stats |
Dominios /api/v1/gtm/:id/domains
| Método | Path | Descripción |
|---|---|---|
| GET | /domains | Listar dominios del GTM (paginado) |
| POST | /domains/add | Agregar dominio custom |
| POST | /domains/update | Actualizar dominio primario |
| DELETE | /domains/remove | Eliminar dominio |
| POST | /domains/:domain/verify | Verificar DNS + disparar generación de cert |
Integración de Ads /api/integrations/ads
| Método | Path | Descripción |
|---|---|---|
| GET | / | Listar integraciones del workspace (status, plataforma, última sync) |
| POST | /:platform/connect | Iniciar OAuth flow (google_ads, meta_ads, tiktok_ads) |
| GET | /:platform/callback | Callback OAuth → guarda tokens → redirige al dashboard |
| DELETE | /:platform | Revocar integración y eliminar tokens |
| GET | /accounts | Listar ad accounts disponibles de todas las integraciones |
| POST | /sync | Forzar sync manual (rate-limited: 1 por hora por workspace) |
| GET | /performance | Métricas agregadas — params: startDate, endDate, platform?, campaignId? |
| GET | /performance/comparison | Métricas de plataforma vs. conversiones Crimoo server-side |
Integración de Plataformas de Ads (Read-Only)
Módulo planificado que conecta las cuentas de Google Ads, Meta Ads y TikTok Ads de los usuarios de Crimoo para hacer pull de métricas de performance publicitario. Los datos se almacenan en ClickHouse junto con los eventos de GTM, habilitando comparaciones directas entre atribución de plataforma vs. atribución server-side de Crimoo.
Estado: Planificado — no implementado aún.
Motivación
Crimoo ya rastrea conversiones server-side (vía GTM) y las envía a plataformas publicitarias. El paso natural es también leer las métricas que esas plataformas reportan, para poder comparar:
| Fuente | Conversiones reportadas |
|---|---|
| Google Ads / Meta / TikTok | Las que ellas atribuyen (click-based, view-through) |
| Crimoo GTM (server-side) | Las que Crimoo rastreó con datos de primera parte |
Esta diferencia es el gap de atribución — uno de los insights más valiosos para un anunciante.
Plataformas y fases
| Fase | Plataforma | Justificación |
|---|---|---|
| 1 | Google Ads | Google OAuth ya existe en Crimoo — menor fricción |
| 2 | Meta Ads | Alta demanda, API estable |
| 3 | TikTok Ads | Audiencias jóvenes, crecimiento sostenido |
Arquitectura
Modelo de datos
PostgreSQL — Configuración de integración
-- Una integración por workspace + plataforma
CREATE TABLE ad_integrations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id UUID NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
platform VARCHAR(20) NOT NULL, -- 'google_ads' | 'meta_ads' | 'tiktok_ads'
status VARCHAR(20) NOT NULL DEFAULT 'active', -- 'active' | 'expired' | 'revoked'
access_token TEXT NOT NULL, -- AES-256-GCM encriptado
refresh_token TEXT, -- AES-256-GCM encriptado
token_expires_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
UNIQUE (workspace_id, platform)
);
-- Cuentas publicitarias vinculadas (una integración puede tener varias)
CREATE TABLE ad_accounts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
integration_id UUID NOT NULL REFERENCES ad_integrations(id) ON DELETE CASCADE,
platform_account_id VARCHAR(100) NOT NULL,
name VARCHAR(255),
currency CHAR(3),
last_synced_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT now()
);
ClickHouse — Métricas diarias
CREATE TABLE ad_performance_daily (
workspace_id UUID,
integration_id UUID,
platform LowCardinality(String), -- 'google_ads' | 'meta_ads' | 'tiktok_ads'
account_id String,
campaign_id String,
campaign_name String,
date Date,
spend Decimal(12, 4),
impressions UInt64,
clicks UInt64,
conversions Float64, -- reportadas por la plataforma
revenue Decimal(12, 4),
currency FixedString(3),
inserted_at DateTime DEFAULT now()
) ENGINE = ReplacingMergeTree(inserted_at)
PARTITION BY toYYYYMM(date)
ORDER BY (workspace_id, platform, account_id, campaign_id, date);
ReplacingMergeTreepermite re-sincronizar el mismo día sin duplicados — el sync idempotente sobrescribe por(workspace_id, platform, account_id, campaign_id, date).
OAuth por plataforma
Google Ads — Reutiliza el Google OAuth existente (/api/auth/google) con scopes adicionales: https://www.googleapis.com/auth/adwords. Acceso vía google-ads-api Node.js client con Customer ID seleccionado.
Meta Ads — OAuth independiente a graph.facebook.com/oauth/authorize. Scopes: ads_read, ads_management. Token de larga duración (60 días), renovado automáticamente por OAuthTokenService.
TikTok Ads — OAuth independiente a business-api.tiktok.com/portal/auth. Scopes: ad.read. Access token de 24h + refresh token de 30 días.
Flujo de sincronización
Rango de sync: Últimos 7 días en cada ciclo (no solo ayer) — corrige datos retroactivos de las plataformas (atribución diferida, view-through tardío).
Vista en el Frontend
Nuevo tab "Ad Performance" en el dashboard de Crimoo.
KPIs superiores: Gasto total · Impresiones · Clicks · CTR · CPC promedio · ROAS
Tabla de campañas:
| Columna | Fuente |
|---|---|
| Campaña | Plataforma |
| Plataforma | Plataforma |
| Gasto | Plataforma |
| Clicks | Plataforma |
| Conv. (Plataforma) | Plataforma |
| Conv. (Crimoo) | ClickHouse — eventos GTM server-side |
| Gap de atribución | (Conv. Plataforma - Conv. Crimoo) / Conv. Plataforma |
| ROAS | Revenue / Spend |
Si no hay integraciones → wizard con botones por plataforma para iniciar OAuth.
Seguridad
- Tokens OAuth almacenados con AES-256-GCM (mismo mecanismo que
PlatformCredentialen conversiones offline) - Los tokens nunca se exponen al frontend — solo
statusytoken_expires_at - Al revocar, se eliminan tokens de PostgreSQL y se llama al endpoint de revocación de la plataforma si existe
- Scopes solicitados son mínimos (solo lectura)
Variables de entorno nuevas
| Variable | Descripción |
|---|---|
GOOGLE_ADS_DEVELOPER_TOKEN | Token de desarrollador requerido por Google Ads API |
META_APP_ID | App ID de Meta for Developers |
META_APP_SECRET | App Secret de Meta |
TIKTOK_APP_ID | App ID de TikTok for Business |
TIKTOK_APP_SECRET | App Secret de TikTok |
ADS_OAUTH_REDIRECT_BASE_URL | Base URL para callbacks OAuth (ej: https://api.crimoo.com) |
Copilot Sessions /api/sessions
| Método | Path | Auth | Descripción |
|---|---|---|---|
| POST | / | JWT | Crear sesión → retorna initToken (120s) |
| POST | /claim | No | Canjear initToken → activationToken |
| POST | /:id/heartbeat | activationToken | Ping para mantener sesión viva |
| POST | /:id/disconnect | activationToken | Desconexión explícita |
| GET | /:id/stream | No | SSE — estado en tiempo real |
| GET | /gtm.js | No | Script loader que inyecta el iframe del copilot |
Ver flujo completo en Flujo de Sesión — Crimoo Copilot.
Endpoints internos /internal/fabric/*
Solo accesibles desde gtm-fabric (sin autenticación, red interna vía Tailscale):
| Método | Path | Descripción |
|---|---|---|
| GET | /fabric/gtms?vmId=X | Config de GTMs para una VM |
| PATCH | /fabric/containers/:id/ports | Reportar puertos tras provisioning |
| POST | /fabric/contacts/batch | Batch flush de contactos CRM |
| POST | /fabric/usage | Reportar usage por GTM |
| POST | /fabric/events | Eventos de ciclo de vida |
| PUT | /fabric/certificates | Reportar cert generado/renovado |
| POST | /fabric/acme-accounts | Guardar cuenta ACME de la VM |
| GET | /fabric/domains?vmId=X&status=Y | Dominios filtrados por VM y status |
| PATCH | /fabric/domains/:domain/status | Actualizar status de dominio (ACTIVE/FAILED) |
Scheduler: Renovación de certificados
CertificateRenewalScheduler corre diario a las 2:00 AM UTC:
- Busca certs en PostgreSQL con
expires_at < 30 díasyauto_renew = true - Agrupa por GTM → VM → ACME account (batch, evita queries repetidas)
- Llama a fabric
POST /certificates/renewen paralelo por cada cert - Fabric ejecuta ACME, escribe PEM, notifica proxy, devuelve cert nuevo
- Actualiza PostgreSQL con los PEM y fechas nuevas
Flujo de Billing (Stripe)
Deployment
node-api corre en producción en la Hostinger VPS (Tailscale: 100.97.60.119) dentro de Docker, junto a PostgreSQL y ClickHouse en la red crimoo_net.
Dockerfile
Se usa Dockerfile.prod (no el multi-stage Dockerfile). El dist/ se compila localmente y se sube por scp — compilar TypeScript dentro del container supera la RAM disponible en la VPS.
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY dist ./dist
EXPOSE 3000
CMD ["node", "dist/app.js"]
Docker Compose
Definido en deployment/docker-compose.prod.yml. El servicio node-api depende de que postgres y clickhouse estén healthy antes de arrancar.
node-api:
build:
context: ..
dockerfile: Dockerfile.prod
container_name: crimoo_node_api
depends_on:
postgres: {condition: service_healthy}
clickhouse: {condition: service_healthy}
ports:
- "3000:3000"
volumes:
- /opt/crimoo/gcp-credentials.json:/app/gcp-credentials.json:ro
env_file:
- .env.prod
networks:
- crimoo_net
Acceso público
node-api no está expuesto directamente a internet. El flujo es:
ui-angular → Cloudflare (api.crimoo.com) → gtm-proxy :443 → node-api :3000
Variables de entorno clave (prod)
| Variable | Valor |
|---|---|
DB_HOST | postgres (nombre del servicio Docker) |
CLICKHOUSE_HOST | clickhouse (nombre del servicio Docker) |
GTM_FABRIC_URL | http://100.97.60.119:8000 (Tailscale) |
GOOGLE_APPLICATION_CREDENTIALS | /app/gcp-credentials.json |
Actualizar en producción
cd /opt/node-api
git pull origin main
npm run build # local, luego scp dist/ a la VPS
docker compose -f deployment/docker-compose.prod.yml up -d --build node-api
Ver node-api/deployment/DEPLOY.md en el workspace para el proceso completo.