Saltar a contenido

Webapps Architecture Mvp

Camunda divide la plataforma en 6 componentes UI + connectors. El MVP los reduce a 2-3 (Operate-equivalent + Tasklist-equivalent + Identity minimal), elimina Optimize (queries SQL + Grafana cubren 80%), reusa Camunda Modeler externo, y deja conectores como SDK simple de workers. Direct queries contra Postgres reemplazan el doble-store de Elasticsearch, dando real-time visibility como ventaja inherente sobre Camunda 8. Esta página detalla qué construir, qué simplificar, qué eliminar, y cómo escalar cada uno.

Mapa estratégico

Camunda 8 tiene 6 webapps + connectors. El MVP necesita decisiones explícitas para cada uno:

Componente Camunda Estrategia MVP Razón
Operate BUILD simplified Monitoring es core requirement
Tasklist BUILD simplified Human tasks son first-class de Camunda
Optimize SKIP SQL ad-hoc + Grafana cubren 80%, ROI no justifica
Identity BUILD minimal Auth es required, pero NO el UI complejo
Modeler (Web/Desktop) REUSE Camunda's BPMN XML es estándar, no rebuild
Connector Runtime SKIP Workers + SDK simple cubre el caso

Decisión clave: NO rebuilds gratuitos. Cada componente tiene un costo de mantenimiento — solo construir lo que aporta valor único.

1. Operate-equivalent — Process Monitoring

Qué construir

Operate de Camunda tiene 100K+ LOC de React + Spring Boot + queries complejos a ES. El MVP necesita ~10K LOC para cubrir el 80% del valor:

Features prioritarias (must-have): - Lista de process instances filtrable (state, processDefinitionId, dateRange, businessId) - Detalle de una instance: tree path, current elements, variables, history de eventos - Lista de incidents con resolución manual - Batch operations: cancel múltiples instances, resolve múltiples incidents - Tiempo real (auto-refresh)

Features eliminables (nice-to-have, posponer): - Process modification UI (cancel/activate elementos individuales en runtime) - Migration UI - Visual BPMN viewer con overlay de progress (complejo de implementar) - Heat maps de elements - Drill-down de sub-procesos visualmente

Arquitectura simplificada

flowchart TD
    FE[Frontend React/Vue/Svelte<br/>Lista de instances, Detalle + variables, Incidents]
    FE -->|REST API| API[Monitoring API engine<br/>GET /v2/process-instances<br/>GET /v2/incidents<br/>POST /v2/operations/batch]
    API --> PG[(PostgreSQL state tables<br/>process_instances, element_instances<br/>variables, incidents, event_log history)]

Diferencia clave vs Camunda: queries van directo al state store, no a un secondary index. Esto da: - Real-time data (no export lag) - Strong consistency - Menos infraestructura (no ES)

Schema queries clave

-- Lista de active instances con filtros
SELECT pi.process_instance_key, pi.bpmn_process_id, pi.start_date, 
       pi.state, COUNT(i.incident_key) FILTER (WHERE i.state = 'ACTIVE') AS incidents
FROM process_instances pi
LEFT JOIN incidents i ON i.process_instance_key = pi.process_instance_key
WHERE pi.tenant_id = $1
  AND pi.state IN ('ACTIVE', 'INCIDENT')
  AND pi.bpmn_process_id = COALESCE($2, pi.bpmn_process_id)
GROUP BY pi.process_instance_key, pi.bpmn_process_id, pi.start_date, pi.state
ORDER BY pi.start_date DESC
LIMIT 50 OFFSET $3;

-- Detalle de una instance con tree path
WITH RECURSIVE tree AS (
    SELECT element_instance_key, parent_scope_key, element_id, state, 1 AS depth
    FROM element_instances
    WHERE process_instance_key = $1 AND parent_scope_key IS NULL

    UNION ALL

    SELECT ei.element_instance_key, ei.parent_scope_key, ei.element_id, ei.state, tree.depth + 1
    FROM element_instances ei
    JOIN tree ON ei.parent_scope_key = tree.element_instance_key
)
SELECT * FROM tree ORDER BY depth, element_instance_key;

Con índices apropiados (tenant_id, state, process_instance_key, parent_scope_key), estas queries son sub-100ms para state stores de millones de instances.

Real-time updates

Dos opciones:

Opción A — Polling (simple):

// Cliente
setInterval(() => fetchProcessInstance(pid), 5000);

Funciona, ineficiente para muchos usuarios concurrentes.

Opción B — WebSocket / SSE con LISTEN/NOTIFY:

-- En el engine, al emit event
NOTIFY process_instance_update, '{"pid": 123, "intent": "ELEMENT_COMPLETED"}';

// Backend escucha LISTEN, reenvía via WebSocket al cliente correcto
client.query('LISTEN process_instance_update');
client.on('notification', (msg) => websocket.send(msg.payload));

Recomendación MVP: Opción A inicialmente. Opción B en Phase 2 si performance lo requiere.

Scaling a través de las fases

Fase scaling Operate scaling
0 — Single-node API embebida en el engine
1 — HA replicas Operate lee de read replica (lag tolerance OK)
2 — Active-active engines Operate puede correr en cualquier engine instance
3 — Tenant sharding Smart router redirige queries al shard del tenant
4 — Citus Queries distributed transparentemente
5 — Geo-distribuido Operate per-region, lee de su region primary

Read replicas son clave para Operate: queries de monitoring pueden tolerar ~5s de lag y descargan al primary del write traffic.

Trade-offs vs Camunda Operate

Aspecto Camunda Operate MVP Operate
Data freshness Eventually consistent (~seconds) Real-time (direct queries)
Aggregations complejas Native ES queries SQL (más limitado)
Full-text search Native ES Postgres FTS (suficiente)
Visual BPMN overlay NO (Phase 2)
LOC ~150K ~10K
Backend dependencies ES cluster Postgres (ya existe)

Ventaja del MVP: data fresca gratis porque no hay export pipeline.

2. Tasklist-equivalent — User Tasks

Qué construir

Tasklist de Camunda: ~100K LOC. MVP necesita ~8K LOC.

Features prioritarias: - Lista de tasks asignadas al usuario actual - Lista de tasks disponibles (candidate user/group) - Detalle de task con variables - Claim task (assign to me) - Complete task con result variables - Forms básicos (JSON Schema → form rendering)

Features eliminables: - Forms designer visual (Phase 2, o usar libraries existentes) - Audit log UI (queries SQL son suficientes) - Embedded forms en BPMN (usar formKey/formId externos) - Task delegation UI compleja - SLA visualization

Arquitectura

flowchart TD
    FE[Frontend<br/>Mis tasks, Tasks disponibles, Task detail + form]
    FE --> API[Task API engine<br/>GET /v2/user-tasks/search<br/>POST /v2/user-tasks/key/assignment<br/>POST /v2/user-tasks/key/completion]
    API --> PG[(PostgreSQL<br/>user_tasks, user_task_variables<br/>candidate_users/groups)]

Schema clave

CREATE TABLE user_tasks (
    user_task_key BIGINT PRIMARY KEY,
    process_instance_key BIGINT NOT NULL,
    process_definition_key BIGINT NOT NULL,
    element_id TEXT NOT NULL,
    tenant_id TEXT NOT NULL,
    state TEXT NOT NULL,  -- CREATED | ASSIGNED | COMPLETED | CANCELED
    assignee TEXT,
    candidate_users TEXT[],     -- Postgres array support
    candidate_groups TEXT[],
    form_key TEXT,
    form_id TEXT,
    form_version INT,
    priority INT,
    due_date TIMESTAMPTZ,
    follow_up_date TIMESTAMPTZ,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    completed_at TIMESTAMPTZ
);

CREATE INDEX idx_task_assignee ON user_tasks(assignee) WHERE state IN ('CREATED', 'ASSIGNED');
CREATE INDEX idx_task_candidate_users ON user_tasks USING GIN(candidate_users);
CREATE INDEX idx_task_candidate_groups ON user_tasks USING GIN(candidate_groups);
CREATE INDEX idx_task_tenant_state ON user_tasks(tenant_id, state);

Query "Mis tasks"

SELECT user_task_key, element_id, process_instance_key, 
       priority, due_date, created_at
FROM user_tasks
WHERE tenant_id = $1
  AND state IN ('CREATED', 'ASSIGNED')
  AND (
    assignee = $2  -- assigned to me
    OR (
      assignee IS NULL  -- claimable
      AND ($2 = ANY(candidate_users) OR $3 && candidate_groups)
    )
  )
ORDER BY priority DESC, due_date ASC NULLS LAST
LIMIT 100;

GIN indexes hacen array containment queries (@>, &&) sub-100ms incluso con millones de tasks.

Forms — estrategia minimal

Camunda tiene 3 tipos de forms: 1. Embedded en BPMN — XML inline 2. Deployed forms — formId + formVersion 3. External forms — formKey link

MVP recomendación: solo JSON Schema forms stored as resources:

{
  "formId": "loan-application-v1",
  "schema": {
    "type": "object",
    "properties": {
      "amount": { "type": "number", "minimum": 1000 },
      "purpose": { "type": "string", "enum": ["personal", "business"] }
    },
    "required": ["amount", "purpose"]
  },
  "uiSchema": { /* react-jsonschema-form syntax */ }
}

Renderizable con react-jsonschema-form o equivalente. Validation server-side via Ajv.

Cero código de form designer en el MVP. Las personas que diseñan forms usan tools standard.

Scaling

Tasklist sigue el mismo patrón que Operate: - Read replicas para queries de listado - WebSocket/SSE en Phase 2 si hay >100 usuarios concurrentes - Tenant sharding transparent en Phases 3+

Caso particular: claim race conditions. Cuando 2 usuarios intentan claim el mismo task:

-- Atomic claim via UPDATE returning
UPDATE user_tasks
SET assignee = $1, state = 'ASSIGNED', updated_at = NOW()
WHERE user_task_key = $2 
  AND state = 'CREATED' 
  AND assignee IS NULL
RETURNING user_task_key;

Si RETURNING es null → alguien más lo claimeó. Aplicar fail message al usuario.

3. Optimize — SKIP, alternative con SQL + Grafana

Por qué skip

Optimize es 300K+ LOC dedicado a BI/analytics de procesos. ROI: - 25-50% throughput reduction cuando está habilitado (ver analysis/sizing-benchmarks) - Importer paralelo de zeebe-record-* indices - UI compleja con report builder, dashboards, alerts

Para 80% de los casos, queries SQL ad-hoc + Grafana cubren todo.

Alternativa MVP

Dashboards Grafana sobre Postgres

-- Métrica: instances iniciadas por hora
SELECT 
  date_trunc('hour', start_date) AS hour,
  COUNT(*) AS instances_started
FROM process_instances
WHERE tenant_id = $tenant
  AND start_date >= NOW() - INTERVAL '7 days'
GROUP BY hour
ORDER BY hour;

-- Métrica: tasks completados promedio por proceso
SELECT 
  pi.bpmn_process_id,
  AVG(EXTRACT(EPOCH FROM (pi.end_date - pi.start_date))) AS avg_duration_seconds,
  COUNT(*) AS total_completed
FROM process_instances pi
WHERE pi.state = 'COMPLETED'
  AND pi.end_date >= NOW() - INTERVAL '30 days'
GROUP BY pi.bpmn_process_id;

-- Bottleneck detection: dónde tarda más cada proceso
SELECT 
  pi.bpmn_process_id,
  ei.element_id,
  AVG(EXTRACT(EPOCH FROM (ei.completed_at - ei.activated_at))) AS avg_duration_seconds
FROM element_instances ei
JOIN process_instances pi ON pi.process_instance_key = ei.process_instance_key
WHERE ei.completed_at IS NOT NULL
  AND ei.activated_at >= NOW() - INTERVAL '7 days'
GROUP BY pi.bpmn_process_id, ei.element_id
HAVING AVG(EXTRACT(EPOCH FROM (ei.completed_at - ei.activated_at))) > 10
ORDER BY avg_duration_seconds DESC;

Grafana datasource es Postgres directo. Dashboards versionables en git. Cero overhead operacional adicional.

Cuándo SÍ construir analytics

Triggers para revisitar (Phase 4+): - Necesidad de process mining real (path discovery, conformance checking) - Branch analysis automático (qué % toma cada path en un gateway) - Outlier detection (instances que tardan 3x el promedio) - Predictive analytics (ETA de completion basado en current state)

Opciones cuando llegues ahí: 1. Construir module dedicado — complejo (esto es lo que Optimize hace) 2. Integrar con tool especializado — Celonis, ProcessGold, Disco 3. OLAP separado — exportar a ClickHouse/BigQuery para analytics queries

Recomendación: defer hasta que el negocio lo pida explícitamente. Para MVP y early growth, Grafana es suficiente.

4. Identity — RBAC minimal + OIDC

Qué construir

Camunda Identity: ~50K LOC con full UI de user management. MVP necesita ~3-5K LOC.

Features must-have: - OIDC integration (single IdP) - API keys para machine-to-machine - RBAC simple: roles admin, operator, worker - Authorization checks en API endpoints

Features skip: - UI completa de user management (usar el IdP) - Multi-IdP support (Phase 2) - Fine-grained permissions (20 resource types × 40 permissions del Camunda original) - Mapping rules complejas (JSONPath)

Schema simplificado

-- Tenants
CREATE TABLE tenants (
    tenant_id TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- User → tenant role
CREATE TABLE user_tenants (
    user_id TEXT NOT NULL,           -- sub claim from OIDC
    tenant_id TEXT REFERENCES tenants(tenant_id),
    role TEXT NOT NULL CHECK (role IN ('admin', 'operator', 'worker')),
    PRIMARY KEY (user_id, tenant_id)
);

-- API keys para workers
CREATE TABLE api_keys (
    api_key_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id TEXT REFERENCES tenants(tenant_id),
    name TEXT NOT NULL,
    key_hash TEXT NOT NULL UNIQUE,    -- bcrypt o argon2 hash
    role TEXT NOT NULL,
    expires_at TIMESTAMPTZ,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    last_used_at TIMESTAMPTZ
);

Authorization model

Solo 3 roles:

Role Permisos
admin Todo en el tenant (deploy, manage, monitor)
operator Monitor + resolve incidents + cancel instances
worker Activate jobs + complete + fail + throw error

Esto es ~5% de la complejidad del modelo de Camunda (20 resource types × 40 permissions) pero cubre 95% de los casos reales.

OIDC flow

sequenceDiagram
    participant C as Cliente
    participant IdP as IdP (Auth0/Okta/Keycloak)
    participant FE as Frontend
    participant E as Engine

    C->>IdP: Auth request
    IdP-->>FE: Authorization code
    FE->>E: POST /v2/auth/token (code)
    E->>IdP: Token exchange
    IdP-->>E: JWT del usuario
    E-->>FE: Session token
    FE->>E: API request con session token
    Note over E: Valida session, resuelve user_id,<br/>busca tenant + role en user_tenants

Scaling

Identity es stateless — scale horizontal trivial. Las decisions vienen de tablas Postgres.

JWT validation puede cachearse (~5 minutos) para reducir hits al IdP.

5. Modeler — REUSE Camunda's

Por qué no rebuild

Camunda Web Modeler + Desktop Modeler son proyectos de millones de LOC con years de polish. Reconstruir sería: - ~12+ meses de inversión - Inferior al original - Sin diferenciación de valor

BPMN XML es el contrato, no la UI. El MVP necesita aceptar BPMN XML válido — no importa quién lo genera.

Opciones para el usuario

Tool Cost Fit
Camunda Web Modeler (SaaS) Paid después de trial Best UX, integrated
Camunda Desktop Modeler Free Local, full features
bpmn.io (open-source) Free Library para embeber
Online BPMN editors (varios) Free Casual users

Recomendación: el MVP incluye un BPMN viewer (read-only) embebido para Operate-equivalent, usando bpmn-js. Para crear/editar, usuarios usan Camunda Modeler.

import BpmnViewer from 'bpmn-js';

const viewer = new BpmnViewer({ container: '#canvas' });
viewer.importXML(bpmnXml);
// Overlay current element state via state from API

bpmn-js es MIT licensed — usable freely.

BPMN XML upload flow

flowchart TD
    Design[Usuario diseña en Camunda Modeler]
    Design -->|Save .bpmn file| Upload[Upload via UI o CLI del MVP]
    Upload -->|POST /v2/deployments| Engine[Engine valida + parsea + persiste]
    Engine --> Available[Process disponible para createInstance]

Cero código de modeling en el MVP. Win arquitectónico.

6. Connectors — SDK simple

Ya cubierto en concepts/connector-sdk-architecture. Resumen para esta página:

Estrategia MVP

Aspecto MVP
Outbound connectors Worker SDK minimal (no framework custom)
Inbound connectors SKIP completamente
Secret providers Env vars (no plugin system)
Element template generator SKIP (usuarios escriben BPMN directamente)
Connector runtime SKIP (workers son apps standalone)

Worker SDK ejemplo

// SDK minimal MVP
import { WorkflowClient, JobWorker } from '@mvp/sdk';

const client = new WorkflowClient({
  endpoint: 'https://workflow.example.com',
  apiKey: process.env.WORKFLOW_API_KEY
});

const worker = new JobWorker({
  client,
  jobType: 'send-email',
  handler: async (job) => {
    const { to, subject, body } = job.variables;
    await sendEmail({ to, subject, body });
    return { messageId: 'abc-123' };  // result variables
  },
  concurrency: 10,
  timeout: 30000
});

await worker.start();

Eso es todo. No connectors registry, no element templates, no secret providers. Si el usuario quiere usar Vault, lo hace en su worker manualmente.

Inbound (eventos externos)

NO inbound connectors. Usuarios usan APIs directas:

// Mi webhook handler personal
app.post('/webhook/payment', async (req, res) => {
  await client.publishMessage({
    name: 'payment-received',
    correlationKey: req.body.orderId,
    variables: { amount: req.body.amount }
  });
  res.status(200).send();
});

Esto es un inbound connector, pero el usuario lo controla. Cero abstracción innecesaria.

Cómo las webapps fit en el scaling strategy

Fase Webapps deployment
0 — Single-node Webapps embedded en el engine process (mismo binary, mismo Postgres)
1 — HA replicas Webapps leen de read replica (5s lag aceptable para monitoring)
2 — Active-active Webapps en cualquier engine instance, load balanced
3 — Tenant sharding Smart router en la API routea queries al shard del tenant
4 — Citus Webapps queries son SQL normal, Citus distribuye
5 — Geo-distribuido Webapps per-region, leen de region primary

Webapps son siempre stateless — escalan trivialmente. Toda la state está en Postgres.

Comparación overall MVP vs Camunda 8

Componente Camunda 8 LOC MVP LOC Reducción
Operate ~150K ~10K 93%
Tasklist ~100K ~8K 92%
Optimize ~300K 0 (Grafana) 100%
Identity ~50K ~3-5K 90%
Modeler ~500K+ 0 (reuse) 100%
Connectors ~100K ~2K (SDK) 98%
Total webapps ~1.2M ~25K ~98%

Sumado al engine (~80K LOC) y plumbing (~15K LOC), el MVP total es ~120K LOC vs ~2M LOC de Camunda 8 — reducción de 94%.

Ventajas inherentes del approach MVP

1. Real-time visibility

Camunda's Operate muestra data eventually consistent (export pipeline lag). El MVP muestra data real-time porque queries van directo al state.

Impacto operacional: - "¿Esta instance completó?" — respuesta inmediata - Debugging es más fácil (no "wait 30 seconds for ES to catch up") - Incidents aparecen al instante

2. Single storage layer

Solo Postgres a operar. No ES + RocksDB + secondary indices. Esto: - Reduce 50%+ infraestructura - Reduce 50%+ monitoring surface - Reduce 50%+ failure modes - Reduce 50%+ on-call burden

3. SQL es expressivo

Queries ad-hoc para responder preguntas de negocio nuevas — sin pipelines de export para construir.

-- "¿Qué procesos tienen el incident type X más frecuente esta semana?"
SELECT pi.bpmn_process_id, COUNT(*) AS incident_count
FROM incidents i
JOIN process_instances pi ON pi.process_instance_key = i.process_instance_key
WHERE i.error_type = 'JOB_NO_RETRIES'
  AND i.created_at >= NOW() - INTERVAL '7 days'
GROUP BY pi.bpmn_process_id
ORDER BY incident_count DESC;

En Camunda esto requiere knowing ES query DSL o Optimize.

4. Backup-restore es atomic

Backup the Postgres = backup todo (engine state, history, user tasks, incidents, identity). No coordination cross-system.

Limitaciones conscientes del MVP

Honestamente, el MVP NO tiene:

  • Full-text search sofisticado (Postgres FTS es decent, no es ES)
  • Process mining real (path discovery, conformance) — Optimize sí
  • Visual BPMN editor — Modeler externo necesario
  • 40+ connectors pre-built — usuarios escriben workers
  • Aggregations time-series complejas — Grafana basic
  • Multi-IdP federation — single IdP en Phase 1
  • Fine-grained authorization — 3 roles vs 20 resource types

Cuando alguno de estos se vuelva critical para el negocio, construir incrementally. No prematuro.

Decision framework por componente

flowchart TD
    Operate{Operate básico lista, detalle, incidents?}
    Operate -->|Sí| BO[BUILD ~10K LOC]
    Tasklist{Tasklist básico claim, complete, forms?}
    Tasklist -->|Sí| BT[BUILD ~8K LOC]
    Optimize{Optimize process mining, BI?}
    Optimize -->|Skip| SO[Grafana + SQL ad-hoc]
    Identity{Identity básico OIDC + RBAC simple?}
    Identity -->|Sí| BI[BUILD ~3-5K LOC]
    Modeler{Modeler?}
    Modeler -->|Skip| SM[Reuse Camunda Modeler + bpmn-js viewer]
    Connector{Connector Runtime?}
    Connector -->|Skip| SC[Worker SDK simple, workers normales]

Resumen ejecutivo

Decisión Por qué
Build Operate simple Real-time visibility es ventaja inherente vs Camunda
Build Tasklist simple Human tasks son core de Camunda value prop
Skip Optimize ROI no justifica vs SQL/Grafana
Build Identity minimal Auth es required, complexity completa NO
Reuse Modeler BPMN XML es estándar, NO rebuild gratuito
Skip Connector Runtime Workers cubren el caso, abstracción innecesaria

Total: ~25K LOC para webapps vs ~1.2M de Camunda (98% reducción). Sin sacrificar el core value prop (process monitoring + human tasks + auth + connectors).

La estrategia es NO competir con Camunda en features. Competir en: - Simplicidad operacional (un store vs múltiples) - Real-time visibility (no export lag) - SQL expresivo (queries ad-hoc gratis) - Cost (menos infra)

Si el usuario necesita las features avanzadas de Camunda, usar Camunda. El MVP es para los 80% que no las necesitan.

Referencias