Backend Architecture
The apptor flow backend is a Gradle multi-module Java 21 project running on Micronaut 4.4.2. This page describes every module, how they relate to each other, and the key design decisions within each layer.
Module Dependency Tree
apptor-flow-sdk
Role: Domain model library. All other modules depend on this.
Key contents:
NodeTypeenum — 37 constants mapping JSON type strings to enum values (e.g.,"aiTask"→AI_TASK)Nodebase class — all node model classes extend this- Specialized node classes:
IfElseNode,InputNode,OutputNode,LoopNode,SetVariableNode Variable,LoopCharacteristics,Condition,FormConfigurationNodeEventTypeenum —CREATE,CANCEL,COMPLETE,TIMEOUTProcessDefinition,FlowDefinition— top-level workflow containers
Base Node fields:
String nodeId
String name
NodeType nodeType
List<String> outgoingSequenceFlowIds
List<String> incomingSequenceFlowIds
Map<String, Object> customProperties
Map<String, Variable> outputVariables
Map<String, Variable> inputVariables
String documentation
LoopType loopType // NONE | STANDARD_LOOP | MULTI_INSTANCE
LoopCharacteristics loopCharacteristics
Timeout timeout // {duration, uom, action}
Retry retry // {delay, uom, maxAttempts}
Map<NodeEventType, NodeEventHandler> eventHandlers
apptor-flow-json-parser
Role: Converts raw workflow JSON text into typed domain model objects.
Key classes:
Json2apptorTypeMapper— maps JSONtypestrings toNodeTypeenum constantsNodeContainerParser— top-level parser that dispatches to per-type parsersYourNodeParser— one parser class per node type (e.g.,IfElseNodeParser,AiTaskNodeParser)
Parser flow:
processText (String JSON)
→ Jackson ObjectMapper → raw Map/List
→ NodeContainerParser.parse()
→ per-type NodeParser.parse(rawNode)
→ typed Node subclass (e.g., IfElseNode)
→ FlowDefinition (complete typed graph)
Type mapping examples:
Map.entry("aiTask", NodeType.AI_TASK)
Map.entry("ifElse", NodeType.IF_ELSE)
Map.entry("serviceTask", NodeType.SERVICE_TASK)
Map.entry("setVariable", NodeType.SET_VARIABLE)
Map.entry("loopNode", NodeType.LOOP_NODE)
Map.entry("inputNode", NodeType.INPUT_NODE)
Map.entry("outputNode", NodeType.OUTPUT_NODE)
apptor-actor-framework
Role: Generic actor system. Provides the thread pool and message queue infrastructure for the execution engine.
Concepts:
- Actor — a named, typed message processor with a configurable thread count
- ActorPool — manages a fixed pool of threads for one actor type
- Message — a serializable envelope dispatched to an actor
- Queue — pluggable queue backend (default:
in-memory-distributedbacked by Hazelcast)
Configuration (from flow-config.json):
{
"processActors": [
{ "name": "aiTask", "maxThreads": 1, "queueType": "in-memory-distributed" },
{ "name": "serviceTask", "maxThreads": 2, "queueType": "in-memory-distributed" },
...
]
}
The cacheType: "hazelcast" setting controls the distributed state store used for queue persistence.
apptor-flow-core
Role: The execution engine. Implements all 24 node handlers and manages process lifecycle.
Node Handler Lifecycle
Every node handler extends AbstractNodeHandler and implements:
void execute(NodeExecutionContext context)
The NodeExecutionContext provides:
- Access to the current node definition
- Read/write access to execution variables
- Methods to dispatch the next node(s)
- Access to services (integrations, secrets, etc.)
All 24 Node Handlers
| Handler Class | Node Type | Threads |
|---|---|---|
StartEventHandler | startEvent | 5 |
EndEventHandler | endEvent | 1 |
TaskHandler | task | 2 |
UserTaskHandler | userTask | 2 |
ServiceTaskHandler | serviceTask | 2 |
ScriptTaskHandler | scriptTask | 2 |
AiTaskHandler | aiTask | 1 |
VoiceTaskHandler | voiceTask | 2 |
IfElseNodeHandler | ifElse | 1 |
ExclusiveGatewayHandler | exclusiveGateway | 1 |
InclusiveGatewayHandler | inclusiveGateway | 1 |
ParallelGatewayHandler | parallelGateway | 1 |
SubProcessHandler | subProcess | 1 |
CallProcessHandler | callProcess | 2 |
CallAgentHandler | callAgent | 2 |
LoopNodeHandler | loopNode | 1 |
BoundaryEventHandler | boundaryEvent | 1 |
IntermediateCatchEventHandler | intermediateCatchEvent | 1 |
IntermediateThrowEventHandler | intermediateThrowEvent | 1 |
InputNodeHandler | inputNode | 2 |
OutputNodeHandler | outputNode | 2 |
MemoryActionHandler | memory | 1 |
DomainActionHandler | domainTask | 2 |
SetVariableHandler | setVariable | 2 |
AiTaskHandler — AI Integration Detail
The AiTaskHandler is the most feature-rich handler. It uses LangChain4j to:
- Build the system and user prompt (resolving
{varName}template variables) - Load conversation memory from
ai_memory_conversation/ai_memory_messagetables - Register connected Tool nodes as LangChain4j tools
- Call the configured LLM provider (OpenAI, Anthropic, etc.) via
aiProviderConnectionId - Optionally stream the response to the chat interface (SSE / WebSocket)
- Generate A2UI component definitions if
enableA2UI: true - Write LLM output to the specified output variable
ScriptTaskHandler — GraalVM Integration
Script tasks execute via GraalVM's Polyglot API:
- Supported languages: JavaScript, Python
- Script has access to all workflow variables via the polyglot context
- Output is written back as workflow variables
apptor-flow-api
Role: The Micronaut REST API layer. 41 controllers exposing all platform capabilities.
Controller base paths:
| Controller | Base Path |
|---|---|
| ProcessController | /process |
| TriggerController | /trigger |
| ApiKeyController | /api/keys |
| OrganizationController | /organizations |
| UserController | /users |
| RoleController | /roles |
| IntegrationController | /integrations |
| SecretController | /secrets |
| EnvironmentController | /environments |
| WebhookRegistrationController | /api/webhooks — CRUD for webhook endpoints |
| WebhookIngressController | /webhook/receive/{token} — public inbound receiver; /webhook/resume/{token} — wait node callback |
| WebhookEventController | /api/webhooks/{id}/events — event log, replay, bulk replay |
| WebhookTestController | /api/webhooks/{id}/test — test mode and signature validator |
| WebhookAuditController | /api/admin/audit/webhooks — immutable audit log |
| WebhookAdminController | /api/admin/webhooks — provider registry, system stats |
| WebhookGdprController | /api/webhooks/gdpr — data export and purge |
| RagController | /rag |
| FileStorageController | /files |
| SuperAdminController | /super-admin |
| ChatWorkflowWebSocketController | /ws/chat/{processInstanceId} |
| A2UIActionController | /a2ui/actions |
| VoiceCallbackController | /voice/callback |
| PublishedWorkflowController | /published-workflows |
| PublicWorkflowController | /public/workflows |
| DomainController | /domains |
| ObservabilityController | /observability |
| EnvironmentController | /environments |
Webhook Module
The webhook module spans apptor-flow-core (verification + normalization) and apptor-flow-api (ingress, services, jobs, controllers). It is a dedicated subsystem with its own database tables, encryption, caching, and retry pipeline.
Core Components
| Component | Location | Responsibility |
|---|---|---|
WebhookVerifier (interface) | apptor-flow-core | Verify signature for one auth type |
WebhookVerifierRegistry | apptor-flow-core | Map authType → verifier; DI-discovered |
WebhookNormalizer (interface) | apptor-flow-core | Extract standard fields from provider payload |
WebhookNormalizerRegistry | apptor-flow-core | Map authType → normalizer |
CustomWebhookVerifier | apptor-flow-core | Handles all custom provider auth methods |
CustomProviderConfig | apptor-flow-core | POJO deserialized from custom_config_json |
WaitNodeHandler | apptor-flow-core | Pause flow; register callback token; resume on POST |
WebhookEncryptionService | apptor-flow-api | AES-256-GCM encrypt/decrypt for secrets |
WebhookSecretService | apptor-flow-api | Secret lifecycle: create, rotate, cache, decrypt |
WebhookRegistrationService | apptor-flow-api | CRUD for registrations + Hazelcast cache invalidation |
WebhookProcessorService | apptor-flow-api | Orchestrate verify → normalize → trigger |
WebhookAuditService | apptor-flow-api | Append-only audit log writes |
WebhookNotificationService | apptor-flow-api | In-app notifications (circuit breaker, DLQ) |
WebhookGdprService | apptor-flow-api | GDPR data export and hard purge |
WebhookRetrySchedulerJob | apptor-flow-api | Re-queue PROCESSING_FAILED events (JobRunr) |
Database Tables (V153–V159, V165)
| Table | Purpose |
|---|---|
webhook_registration | Endpoint config: name, authType, status, circuit breaker counters |
webhook_secret | AES-256-GCM encrypted secrets; supports multiple ACTIVE for rotation overlap |
webhook_event | Full event log: headers, payload, status, retry tracking, idempotency key |
webhook_execution_token | Wait node callback tokens with timeout tracking |
webhook_audit_log | Append-only 7-year audit trail; secrets never written here |
webhook_provider_registry | Provider lifecycle: BETA → GA → DEPRECATED → REMOVED |
webhook_metrics_hourly | Aggregated counts and latency per org/provider/endpoint |
system_notification | In-app notifications for circuit breaker trips and DLQ accumulation |
system_notification | In-app notifications for circuit breaker trips and DLQ accumulation |
Secret Encryption
Secrets are encrypted with AES-256-GCM. Each encrypted value is stored as Base64(IV[12] || ciphertext || tag[16]). A fresh random IV is generated per encryption call, making ciphertext non-deterministic even for identical secrets. The key is loaded from apptor.secret.encryption-key and normalized to 32 bytes.
Supported Auth Types
| Auth Type | Verification Method |
|---|---|
GITHUB | HMAC-SHA256 of body, sha256= prefix on X-Hub-Signature-256 |
GITLAB | Constant-time string comparison on X-Gitlab-Token |
BITBUCKET | HMAC-SHA256 of body on X-Hub-Signature |
SHOPIFY | HMAC-SHA256 Base64 of body on X-Shopify-Hmac-SHA256 |
WOOCOMMERCE | HMAC-SHA256 Base64 of body on X-WC-Webhook-Signature |
STRIPE | HMAC-SHA256 of {ts}.{body} with ±5 min replay window |
PADDLE | HMAC-SHA256 of {ts}:{body} with ±5 min replay window |
SLACK | HMAC-SHA256 of v0:{ts}:{body} with ±5 min replay window |
RAZORPAY | HMAC-SHA256 of raw body bytes |
SQUARE | HMAC-SHA256 Base64 on x-square-hmacsha256-signature |
STANDARD_WEBHOOKS | Standard Webhooks spec: {webhook-id}.{ts}.{body} |
BEARER_TOKEN | Constant-time comparison of Authorization: Bearer value |
CUSTOM | Reads custom_config_json for method, header, template, format |
NONE | Always passes — no verification |
apptor-flow-persistence
Role: JPA entities, repositories, and Liquibase migration changelogs.
All entities extend BaseEntity which provides organizationId (multi-tenancy), createdAt, and updatedAt.
Key entity classes: ProcessEntity, ProcessVersionEntity, ProcessInstanceEntity, NodeInstanceEntity, ApiKeyEntity, RoleEntity, PermissionEntity, SecretEntity, IntegrationEntity, EnvironmentEntity.
apptor-secrets
Role: Encrypted storage for organization secrets. Values are encrypted at rest. Referenced in workflow properties as {env.SECRET_NAME}.
apptor-flow-vertical-integrations
Role: Provider-agnostic domain integration framework.
Concepts:
- Vertical — a business domain (e.g., CRM, HRIS, Accounting)
- Domain Action — an abstract operation within a vertical (e.g., "Create Contact")
- Provider Mapping — maps the abstract action to a specific provider's API (e.g., Salesforce, HubSpot)
- Integration Version — a versioned snapshot of a provider mapping
The Domain Task node executes a specific domain action via the configured provider mapping. This enables workflows to be provider-agnostic — swapping from Salesforce to HubSpot only requires updating the provider mapping, not the workflow.