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):
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 | Sí | 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¶
- entities/operate — Operate de Camunda detallado
- entities/tasklist — Tasklist de Camunda detallado
- entities/optimize — Optimize de Camunda detallado
- entities/identity — Identity de Camunda detallado
- entities/connectors — Connector runtime detallado
- analysis/scaling-strategy-postgres — Cómo cada componente escala en cada fase
- analysis/mvp-feature-matrix — Tier de features (essential / simplifiable / eliminable)
- concepts/connector-sdk-architecture — Detalles del SDK pattern
- concepts/search-infrastructure — Por qué el MVP elimina la search abstraction