Technical Architecture
Deep dive into the internals of Automation Hub — from event-driven workflows to multi-tenant isolation, queue management, and the Module SDK.
Technology Stack
| Layer | Technology | Version | Purpose |
|---|---|---|---|
| Backend Framework | Laravel | 12.x | API, routing, queue, ORM, middleware |
| Frontend Framework | React | 19.x | SPA UI with hooks and concurrent features |
| Bridge | Inertia.js | 2.x | Server-driven SPA without REST boilerplate |
| Styling | Tailwind CSS | v4 | Utility-first CSS with design tokens |
| Database | MySQL | 8.x | Primary relational data store |
| Language | TypeScript | 5.x | Type-safe frontend development |
| Canvas Editor | @xyflow/react | v12 | Visual workflow drag-and-drop canvas |
| Build Tool | Vite | 6.x | Hot module replacement and bundling |
| Queue | Laravel Queue (database) | - | Async job processing |
| Auth | Laravel Fortify + Sanctum | - | Session auth, 2FA, API tokens |
Event-Driven Architecture
Automation Hub follows an event-driven architecture where all workflow executions are triggered by events. The system decouples event producers from consumers through an internal Event Bus.
┌──────────────────────────────────────────────────────────────────────────┐
│ EVENT SOURCES │
├──────────────┬──────────────┬──────────────┬──────────────┬──────────────┤
│ Webhook │ Schedule │ API Call │ Module │ Manual │
│ Trigger │ (cron) │ Trigger │ Event │ Trigger │
└──────┬───────┴──────┬───────┴──────┬───────┴──────┬───────┴──────┬───────┘
│ │ │ │ │
└──────────────┴──────────────┴──────────────┴──────────────┘
│
▼
┌────────────────────────┐
│ EventBus │
│ (App\Services\EventBus)│
│ │
│ • Receives events │
│ • Finds matching │
│ workflow triggers │
│ • Dispatches jobs │
└───────────┬────────────┘
│
▼
┌────────────────────────┐
│ ProcessEventJob │
│ (Queued, async) │
│ │
│ • Creates execution │
│ • Invokes engine │
└───────────┬────────────┘
│
▼
┌────────────────────────┐
│ WorkflowEngine │
│ │
│ • Topological sort │
│ • Execute nodes │
│ • Handle conditions │
│ • Pause/resume │
│ • Error handling │
└────────────────────────┘
Why Event-Driven?
Event-driven architecture allows workflows to react to external stimuli in real-time without polling. It also enables loose coupling between modules — a WhatsApp message, an email bounce, or a webhook hit all flow through the same pipeline.
Multi-Tenant Model
Automation Hub uses a shared-database, shared-schema multi-tenancy approach. Every tenant-scoped table includes an organization_id column, and data isolation is enforced at the query level via a Laravel global scope.
// app/Scopes/OrganizationScope.php
class OrganizationScope implements Scope
{
public function apply(Builder $builder, Model $model): void
{
if (auth()->check() && auth()->user()->current_organization_id) {
$builder->where(
$model->getTable() . '.organization_id',
auth()->user()->current_organization_id
);
}
}
}
// Usage in any tenant-scoped model:
class Workflow extends Model
{
protected static function booted(): void
{
static::addGlobalScope(new OrganizationScope);
}
}
Key characteristics of the multi-tenant model:
- Automatic scoping: All queries on tenant models are automatically filtered by
organization_id - Creation enforcement: A model observer automatically sets
organization_idon record creation - Cross-tenant prevention: Middleware validates that requested resources belong to the user's current organization
- Organization switching: Users can belong to multiple organizations and switch context via the UI
Module SDK Pattern
Modules are self-contained feature packages that extend Automation Hub's capabilities. Each module implements the ModuleInterface contract and registers its components through the ModuleSDK facade.
interface ModuleInterface
{
public function identifier(): string; // e.g., 'email', 'whatsapp'
public function name(): string; // Human-readable name
public function version(): string; // Semver
public function description(): string;
public function register(ModuleSDK $sdk): void;
public function boot(): void;
public function getConfigSchema(): array; // JSON Schema for settings
public function validateConfig(array $config): bool;
}
class EmailModule implements ModuleInterface
{
public function register(ModuleSDK $sdk): void
{
// Register node executors (action types)
$sdk->registerExecutor('action.email.send', EmailSendExecutor::class);
// Register event listeners
$sdk->registerListener('user.registered', WelcomeEmailListener::class);
// Register permissions
$sdk->registerPermissions([
'email.send',
'email.configure',
'email.view_logs',
]);
// Register routes
$sdk->registerRoutes(__DIR__ . '/../routes/email.php');
// Register configuration schema
$sdk->registerConfigSchema($this->getConfigSchema());
}
}
The SDK registration pattern ensures:
- Isolation: Modules cannot interfere with each other or the core
- Discoverability: The system knows all available node types, events, and permissions at boot
- Hot-swap: Modules can be installed/uninstalled without code changes to the core
- Testability: Each module can be tested in isolation with a mock SDK
Workflow Engine Internals
The WorkflowEngine is the heart of Automation Hub. It takes a workflow definition (a directed acyclic graph of nodes and edges) and executes it deterministically.
Execution Lifecycle
1. EVENT RECEIVED
└─▶ EventBus matches event type to workflow triggers
2. EXECUTION CREATED
└─▶ workflow_executions record: status = 'running'
3. TOPOLOGICAL SORT
└─▶ Nodes ordered by dependency (edges define order)
└─▶ Kahn's algorithm ensures no node runs before its inputs
4. NODE EXECUTION (iterative)
├─▶ Check condition (if conditional node)
├─▶ Resolve variables ({{step_id.output.field}})
├─▶ Call executor (module-provided or core)
├─▶ Store output in execution_steps
└─▶ Determine next nodes (follow edges)
5. PAUSE/RESUME (if delay or wait node)
├─▶ Status → 'paused'
├─▶ ResumeWorkflowJob dispatched with delay
└─▶ On resume: continue from paused step
6. COMPLETION
├─▶ Status → 'completed' or 'failed'
└─▶ Duration and metadata recorded
Topological Sort
The engine uses Kahn's algorithm for topological sorting. This guarantees that nodes are executed in the correct order, respecting all edge dependencies. The algorithm:
- Builds an adjacency list and in-degree count from the workflow edges
- Starts with nodes that have zero in-degree (trigger nodes)
- Processes nodes in BFS order, decrementing in-degree of downstream nodes
- Detects cycles (if any nodes remain unprocessed, the graph is invalid)
Error Handling
When a node execution fails:
- The
execution_stepsrecord is marked asfailedwith error details - If an error handler edge exists, execution continues on the error path
- If no error handler exists, the entire execution is marked as
failed - All step outputs up to the failure point are preserved for debugging
Pause and Resume
Delay nodes and human-approval nodes can pause execution. When paused:
- The execution status is set to
pausedwith metadata about where to resume - A
ResumeWorkflowJobis dispatched with the appropriate delay (e.g., 5 minutes, 1 hour) - When the job fires, execution resumes from the next node after the pause point
- Paused executions can also be manually resumed or cancelled via the API
RBAC System
The Role-Based Access Control system provides granular permission management at the organization level.
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ Users │──M:N──│Organizations │──1:N──│ Roles │
└──────────────┘ └──────────────┘ └───────┬──────────┘
│
pivot: organization_user │ M:N
(user_id, org_id, │
role_id) ▼
┌──────────────────┐
│ Permissions │
│ │
│ • workflows.* │
│ • modules.* │
│ • settings.* │
│ • users.* │
│ • api_keys.* │
└──────────────────┘
| Default Role | Description | Key Permissions |
|---|---|---|
owner | Full control over the organization | All permissions (wildcard) |
admin | Manage users, modules, and workflows | All except billing and org deletion |
editor | Create and edit workflows | workflows.*, modules.view, executions.view |
viewer | Read-only access | *.view only |
Permissions follow a resource.action naming convention (e.g., workflows.create, workflows.delete, modules.configure). Modules can register additional permissions via the SDK.
API Authentication
The REST API uses bearer token authentication with SHA-256 hashed keys stored in the database. Keys are never stored in plain text.
┌─────────────────────────────────────────────────────────────────┐
│ API Key Lifecycle │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. User creates key in UI │
│ └─▶ Plain key shown ONCE: ah_live_abc123... │
│ └─▶ SHA-256 hash stored: hash('sha256', $plainKey) │
│ │
│ 2. Client sends request │
│ └─▶ Header: Authorization: Bearer ah_live_abc123... │
│ │
│ 3. Server validates │
│ └─▶ Hash incoming key │
│ └─▶ Look up hash in api_keys table │
│ └─▶ Check: is_active, expires_at, organization_id │
│ └─▶ Check: key scopes vs. requested endpoint │
│ │
│ 4. Rate limiting applied │
│ └─▶ Per-key limit (e.g., 60 req/min) │
│ └─▶ Headers: X-RateLimit-Remaining, X-RateLimit-Reset │
│ │
└─────────────────────────────────────────────────────────────────┘
Each API key has:
- Scopes: Granular access control (e.g.,
workflows:read,workflows:write,executions:read) - Expiration: Optional expiry date after which the key is rejected
- Rate limit: Configurable per-key requests-per-minute ceiling
- Audit trail: Last used timestamp and IP address recorded
Queue System
Automation Hub uses Laravel's queue system with the database driver for reliable, persistent job processing without external dependencies like Redis.
| Job Class | Queue | Purpose | Retry Policy |
|---|---|---|---|
ProcessEventJob | default | Process incoming events and trigger workflows | 3 attempts, 30s backoff |
DispatchWebhookJob | webhooks | Send outbound webhook HTTP requests | 3 attempts, exponential backoff |
ResumeWorkflowJob | default | Resume paused workflow executions after delay | 1 attempt (delay-based) |
# Process all queues (recommended for production)
php artisan queue:work --queue=default,webhooks --tries=3 --backoff=30
# Process only webhooks (separate worker for isolation)
php artisan queue:work --queue=webhooks --tries=3 --backoff=10,30,60
# Monitor failed jobs
php artisan queue:failed
Queue Worker Required
Workflow executions will not process without a running queue worker. In production, use a process manager like Supervisor to keep the worker running. See the Getting Started guide for Supervisor configuration.
Project Folder Structure
automation-hub/
├── app/
│ ├── Http/
│ │ ├── Controllers/
│ │ │ ├── WorkflowController.php
│ │ │ ├── ExecutionController.php
│ │ │ ├── ModuleController.php
│ │ │ ├── OrganizationController.php
│ │ │ └── Api/
│ │ │ └── V1/
│ │ │ ├── WorkflowApiController.php
│ │ │ ├── EventApiController.php
│ │ │ └── WebhookApiController.php
│ │ ├── Middleware/
│ │ │ ├── EnsureOrganizationAccess.php
│ │ │ ├── ApiKeyAuthentication.php
│ │ │ └── RateLimitByApiKey.php
│ │ └── Requests/
│ ├── Models/
│ │ ├── User.php
│ │ ├── Organization.php
│ │ ├── Workflow.php
│ │ ├── WorkflowExecution.php
│ │ ├── ExecutionStep.php
│ │ ├── Event.php
│ │ ├── Module.php
│ │ ├── ApiKey.php
│ │ └── Role.php
│ ├── Modules/
│ │ ├── Email/
│ │ │ ├── EmailModule.php
│ │ │ ├── Executors/EmailSendExecutor.php
│ │ │ └── Config/email-schema.json
│ │ ├── WhatsApp/
│ │ │ ├── WhatsAppModule.php
│ │ │ ├── Executors/
│ │ │ ├── Models/WhatsAppConversationModel.php
│ │ │ └── Config/whatsapp-schema.json
│ │ ├── AI/
│ │ │ ├── AIModule.php
│ │ │ ├── Executors/AIPromptExecutor.php
│ │ │ └── Config/ai-schema.json
│ │ ├── Notifications/
│ │ │ └── NotificationsModule.php
│ │ └── Courier/
│ │ ├── CourierModule.php
│ │ └── Executors/
│ ├── Scopes/
│ │ └── OrganizationScope.php
│ ├── Services/
│ │ ├── EventBus.php
│ │ ├── WorkflowEngine.php
│ │ └── ModuleSDK.php
│ └── Jobs/
│ ├── ProcessEventJob.php
│ ├── DispatchWebhookJob.php
│ └── ResumeWorkflowJob.php
├── resources/
│ └── js/
│ ├── Pages/
│ │ ├── Dashboard.tsx
│ │ ├── Workflows/
│ │ │ ├── Index.tsx
│ │ │ ├── Editor.tsx
│ │ │ └── Canvas/
│ │ │ ├── WorkflowCanvas.tsx
│ │ │ ├── nodes/
│ │ │ └── edges/
│ │ ├── Executions/
│ │ ├── Modules/
│ │ ├── Settings/
│ │ └── Organizations/
│ ├── Components/
│ │ ├── ui/ (shadcn/ui components)
│ │ ├── layout/
│ │ └── shared/
│ ├── hooks/
│ ├── lib/
│ └── i18n/
│ ├── en.json
│ ├── es.json
│ ├── fr.json
│ └── ar.json
├── database/
│ ├── migrations/
│ │ ├── create_organizations_table.php
│ │ ├── create_workflows_table.php
│ │ ├── create_workflow_executions_table.php
│ │ ├── create_execution_steps_table.php
│ │ ├── create_events_table.php
│ │ ├── create_modules_table.php
│ │ ├── create_api_keys_table.php
│ │ ├── create_roles_table.php
│ │ └── create_permissions_table.php
│ ├── seeders/
│ │ ├── TemplateSeeder.php
│ │ └── RolePermissionSeeder.php
│ └── factories/
├── routes/
│ ├── web.php
│ ├── api.php
│ └── channels.php
├── tests/
│ ├── Feature/
│ └── Unit/
└── config/
└── automation-hub.php
Database Schema Overview
| Table | Purpose | Key Columns |
|---|---|---|
organizations | Tenant container | id, name, slug, settings (JSON), plan |
users | User accounts | id, name, email, current_organization_id, two_factor_enabled |
organization_user | User-org membership pivot | user_id, organization_id, role_id |
roles | Named role definitions | id, name, organization_id, is_default |
permissions | Granular permission list | id, name, group |
role_permission | Role-permission pivot | role_id, permission_id |
workflows | Workflow definitions | id, organization_id, name, definition (JSON), is_active, trigger_type |
workflow_executions | Execution instances | id, workflow_id, status, trigger_data (JSON), started_at, completed_at |
execution_steps | Per-node execution results | id, execution_id, node_id, status, input (JSON), output (JSON), error |
events | Event log | id, organization_id, type, payload (JSON), source |
modules | Installed module registry | id, identifier, name, version, is_active, config (JSON encrypted) |
api_keys | API authentication keys | id, organization_id, name, key_hash, scopes (JSON), rate_limit, last_used_at |
audit_logs | Activity audit trail | id, organization_id, user_id, action, subject_type, subject_id, metadata |
Security Layers
Automation Hub implements defense-in-depth with multiple security layers:
| Layer | Implementation | Details |
|---|---|---|
| CSRF Protection | Laravel CSRF middleware | All state-changing requests require a valid CSRF token |
| Rate Limiting | Per-route + per-API-key | Configurable limits; 429 response with Retry-After header |
| SSRF Protection | Webhook URL validation | Block private IPs (10.x, 172.16-31.x, 192.168.x, 127.x), DNS rebinding prevention |
| Credential Encryption | Laravel's encrypt() | Module configs (API keys, passwords) encrypted at rest with APP_KEY |
| Audit Logging | Model observers + middleware | All CRUD operations, logins, permission changes logged with actor and IP |
| Input Validation | Form Request classes | Every endpoint validates input; workflow definitions validated against JSON schema |
| SQL Injection | Eloquent ORM + parameterized queries | No raw SQL; all user input parameterized |
| XSS Prevention | React auto-escaping + CSP headers | React escapes by default; Content-Security-Policy headers restrict inline scripts |
| 2FA / TOTP | Laravel Fortify | Time-based one-time passwords with recovery codes |
| CRLF Injection | Email header sanitization | Strip \r\n from email headers to prevent header injection |
Security Best Practice
Always keep your APP_KEY secure and backed up. This key is used to encrypt all module credentials (SMTP passwords, API keys, etc.). If lost, you will need to re-configure all modules.