This document describes the core architecture of the Escalated platform -- the patterns, abstractions, and system boundaries that apply across all framework implementations.
Escalated is structured as a set of backend framework packages that share a common Vue 3 frontend and connect to a cross-framework plugin system. Each backend package implements the same interfaces (ticket operations, SLA, escalation, assignment) in the idioms of its target framework.
┌──────────────────────────┐
│ @escalated-dev/escalated│
│ Vue 3 + Inertia.js │
│ Shared frontend │
└────────────┬─────────────┘
│ Inertia protocol
┌──────────┬──────────┬───────────┼───────────┬──────────┬──────────┐
│ Laravel │ Django │ Rails │ AdonisJS │ Phoenix │ Symfony │
│ PHP │ Python │ Ruby │ TypeScript│ Elixir │ PHP │
└────┬─────┴────┬─────┴────┬──────┴────┬──────┴────┬─────┴────┬────┘
│ │ │ │ │ │
└──────────┴──────────┴─────┬─────┴───────────┴──────────┘
│
┌──────┴──────┐
│ Plugin │
│ Runtime │
│ (Node.js) │
└──────┬──────┘
│ JSON-RPC 2.0 / stdio
┌──────┴──────┐
│ Plugin SDK │
│ TypeScript │
└─────────────┘
Every Escalated backend package supports three hosting modes, controlled by a single config value (mode):
All data lives in the host application's database. No external calls. Full data sovereignty.
- Tickets, replies, departments, SLA policies, escalation rules -- all stored locally.
- The
LocalDriverhandles every CRUD operation against the local DB. - Best for teams that want full control and have their own infrastructure.
Data lives locally AND is replicated to the Escalated Cloud API. Enables cross-app visibility, centralized reporting, and the cloud dashboard.
- The
SyncedDriverwrites locally first, then emits sync events tocloud.escalated.dev. - If the cloud is unreachable, operations still succeed locally and sync when reconnected.
- Best for organizations with multiple apps that want a unified view.
All CRUD is proxied to the Escalated Cloud API. The local DB is not used for ticket data.
- The
CloudDriverforwards every operation tocloud.escalated.dev/api/v1. - The host app only handles authentication and UI rendering.
- Best for teams that want zero infrastructure overhead.
Switching is a config change:
// Laravel: config/escalated.php
'mode' => env('ESCALATED_MODE', 'self-hosted'),# Django: settings.py
ESCALATED = { 'MODE': 'self-hosted' }# Rails: config/initializers/escalated.rb
config.mode = :self_hosted// AdonisJS: config/escalated.ts
mode: 'self-hosted',# Phoenix: config/config.exs
config :escalated, mode: :self_hostedThe driver pattern is the core abstraction that makes hosting modes work. Each framework defines a TicketDriver interface (or equivalent) with methods for every ticket operation:
// PHP (Laravel) - Contracts/TicketDriver.php
interface TicketDriver
{
public function createTicket(Ticketable $requester, array $data): Ticket;
public function updateTicket(Ticket $ticket, array $data): Ticket;
public function transitionStatus(Ticket $ticket, TicketStatus $status, ?Ticketable $causer = null): Ticket;
public function assignTicket(Ticket $ticket, int $agentId, ?Ticketable $causer = null): Ticket;
public function unassignTicket(Ticket $ticket, ?Ticketable $causer = null): Ticket;
public function addReply(Ticket $ticket, Ticketable $author, string $body, bool $isNote = false, array $attachments = []): Reply;
public function getTicket(int|string $id): Ticket;
public function listTickets(array $filters = [], ?Ticketable $for = null): LengthAwarePaginator;
public function addTags(Ticket $ticket, array $tagIds, ?Ticketable $causer = null): Ticket;
public function removeTags(Ticket $ticket, array $tagIds, ?Ticketable $causer = null): Ticket;
public function changeDepartment(Ticket $ticket, int $departmentId, ?Ticketable $causer = null): Ticket;
public function changePriority(Ticket $ticket, TicketPriority $priority, ?Ticketable $causer = null): Ticket;
}Three implementations exist:
| Driver | Class | Behavior |
|---|---|---|
LocalDriver |
Drivers/LocalDriver |
Direct DB queries via the ORM |
SyncedDriver |
Drivers/SyncedDriver |
Local DB + async sync to cloud |
CloudDriver |
Drivers/CloudDriver |
HTTP proxy to cloud.escalated.dev |
The active driver is resolved from config at boot. All services and controllers depend on the TicketDriver interface, never a concrete driver.
Business logic lives in dedicated service classes, not in controllers or models. This pattern is consistent across all frameworks:
| Service | Responsibility |
|---|---|
TicketService |
Ticket CRUD, status transitions, filtering |
AssignmentService |
Agent assignment, round-robin, capacity-based routing |
SlaService |
SLA policy evaluation, breach detection, business hours |
EscalationService |
Rule evaluation, automatic escalation actions |
NotificationService |
Email dispatch, webhook delivery |
AttachmentService |
File upload, storage, validation |
MacroService |
Macro execution (batch actions on tickets) |
ReportingService |
Metrics, aggregations, dashboard data |
PluginService |
Plugin lifecycle, hook dispatch to runtime |
InboundEmailService |
Email parsing (Mailgun, Postmark, SES, IMAP) |
ImportService |
Data import from Zendesk, Freshdesk, Intercom, Help Scout |
SkillRoutingService |
Skill-based ticket routing |
CapacityService |
Agent workload and capacity tracking |
WebhookDispatcher |
Outbound webhook delivery with retries |
AutomationRunner |
Trigger-based automation execution |
BusinessHoursCalculator |
Business hours and holiday-aware time calculations |
HookRegistry |
Plugin hook registration and dispatch |
TicketSplitService |
Split a reply into a new linked ticket |
SnoozeService |
Snooze/wake ticket scheduling |
SavedViewService |
CRUD for saved filter views |
WidgetService |
Widget API (KB search, ticket creation, status lookup) |
BroadcastService |
Real-time event broadcasting dispatch |
EmailBrandingService |
Branded email template rendering |
Controllers are thin -- they validate input, call a service, and return a response.
Every framework package supports running in headless mode (no UI). When ui_enabled is false, only the backend is active: API routes, commands, events, migrations, and the plugin runtime.
The UI abstraction is the EscalatedUiRenderer interface:
// PHP (Laravel)
interface EscalatedUiRenderer
{
public function render(string $page, array $props = []): mixed;
}The default implementation (InertiaUiRenderer) delegates to Inertia.js, which renders Vue 3 components from the shared @escalated-dev/escalated npm package. Teams can swap in their own renderer (Blade, Livewire, etc.) by binding a different implementation.
Equivalent abstractions exist in other frameworks:
- Django:
render_page(request, page, props)function - Rails:
Escalated::UiRenderermodule - AdonisJS:
EscalatedRendererclass - Phoenix:
Escalated.Rendererbehaviour - Go:
renderer.Render(w, r, page, props)function
The shared frontend (@escalated-dev/escalated) provides Vue 3 pages and components:
pages/Agent/*-- Agent dashboard, ticket list, ticket detailpages/Customer/*-- Customer portal, ticket submissionpages/Admin/*-- Admin settings, departments, SLA, rulescomponents/*-- Reusable ticket components, editors, filters
The plugin system is a three-layer architecture:
TypeScript SDK for authoring plugins. Provides definePlugin() with support for:
- Actions -- Fire-and-forget event handlers (
ticket.created,ticket.resolved, etc.) - Filters -- Transform data passing through hooks (must return modified value)
- Endpoints -- Custom REST routes exposed by the plugin
- Webhooks -- Incoming webhook handlers
- Cron -- Scheduled tasks (cron expressions)
- Pages -- Full-page UI routes in the admin/agent/customer areas
- Components -- UI components injected into existing page slots
- Widgets -- Dashboard widgets with optional badge counts
- Config -- Admin-configurable settings (text, password, select, etc.)
- Lifecycle --
onActivate/onDeactivatehooks
Every handler receives a PluginContext (ctx) with:
| Property | Purpose |
|---|---|
ctx.config |
Read/write plugin config values |
ctx.store |
Persistent key-value and query store |
ctx.http |
Outbound HTTP client |
ctx.broadcast |
Real-time event broadcasting |
ctx.log |
Structured logging |
ctx.tickets |
Ticket CRUD repository |
ctx.replies |
Reply repository |
ctx.contacts |
Contact lookup/creation |
ctx.tags |
Tag management |
ctx.departments |
Department lookup |
ctx.agents |
Agent lookup |
ctx.emit |
Emit custom hooks |
ctx.currentUser |
Current authenticated user |
A long-lived Node.js process that:
- Discovers installed plugins from
node_modules/@escalated-dev/plugin-* - Responds to JSON-RPC 2.0 messages from the host framework
- Routes hooks (actions, filters) to registered plugin handlers
- Proxies
ctx.*calls back to the host for data operations
Each backend framework has a bridge that:
- Spawns the plugin runtime as a child process
- Sends hook dispatches over stdin (JSON-RPC 2.0)
- Receives and responds to
ctx.*calls from plugins - Handles runtime lifecycle (lazy spawn, restart on crash, exponential backoff)
Host Framework (PHP/Ruby/Python) Plugin Runtime (Node.js)
┌──────────────────────────┐ stdio ┌──────────────────────────┐
│ Bridge │<---------->│ @escalated-dev/ │
│ - spawns subprocess │ JSON- │ plugin-runtime │
│ - dispatches hooks │ RPC 2.0 │ ┌────────────────────┐ │
│ - handles ctx.* calls │ │ │ plugin-slack │ │
│ │ │ │ plugin-jira │ │
│ │ │ │ your-custom-plugin │ │
└──────────────────────────┘ │ └────────────────────┘ │
└──────────────────────────┘
Resilience guarantees:
- Runtime spawned lazily on first hook dispatch
- Automatic restart with exponential backoff on crash
- Action hooks degrade gracefully when runtime unavailable
- Filter hooks return unmodified values when runtime is down
The escalated-plugins monorepo contains all official plugins, each as a pair: an SDK plugin (plugin-*-sdk) and a metadata/README package (plugin-*):
- AI Copilot, Approvals, Community, Compliance, Custom Layouts, Custom Objects
- Import tools (Zendesk, Freshdesk, Intercom, Help Scout)
- Integrations (Slack, Jira, WhatsApp, Social, Phone, SMS)
- Channels (Live Chat, Web Widget, Proactive Messages)
- Utilities (IP Restriction, NPS, Scheduled Reports, Omnichannel Routing, Unified Status, Mobile SDK, Marketplace)
Every significant action dispatches an event. Events are consumed by:
- Internal listeners -- Update SLA timers, trigger escalation checks, send notifications
- Plugin hooks -- Forwarded to the plugin runtime for any registered action/filter handlers
- Webhooks -- Outbound HTTP calls to configured URLs with signed payloads
- Sync events -- In synced mode, replicated to the cloud API
Core events include:
ticket.created,ticket.updated,ticket.status_changedticket.assigned,ticket.unassigned,ticket.escalatedticket.priority_changed,ticket.department_changedticket.tagged,ticket.untagged,ticket.mergedticket.split(new -- when a reply is split into a new ticket)ticket.snoozed,ticket.unsnoozed(new -- snooze lifecycle)reply.created,reply.updatedsla.breached,sla.warningagent.online,agent.offlinerating.submitted
The Escalated Cloud (cloud.escalated.dev) provides:
- REST API (
/api/v1) -- Full CRUD for tickets, replies, departments, tags, SLA policies - Multi-tenant -- Each connected app is a tenant with isolated data
- API key auth -- Bearer tokens with per-key scoped abilities
- Webhook delivery -- Outbound webhooks for cloud events
- Cross-app reporting -- Unified dashboard across all connected apps
- SSO -- Single sign-on across cloud-connected instances
The Cloud API mirrors the TicketDriver interface so that CloudDriver implementations can proxy calls directly.
The plugin marketplace (marketplace.escalated.dev) is the distribution channel for plugins:
- Plugin registry -- Metadata, versions, compatibility matrices
- Installation --
npm install @escalated-dev/plugin-{name}+ activate in admin UI - Configuration -- Per-plugin admin settings defined in plugin
configschema - Versioning -- Semver with framework compatibility constraints
- Categories -- Integrations, Channels, Automation, Analytics, Import, Utilities
The following tables are created by every framework package (prefixed with escalated_):
| Table | Purpose |
|---|---|
escalated_tickets |
Core ticket records |
escalated_replies |
Ticket replies (public, internal notes, system) |
escalated_departments |
Department/team organization |
escalated_tags |
Ticket categorization tags |
escalated_ticket_tag |
Pivot: tickets <-> tags |
escalated_sla_policies |
SLA definitions with per-priority targets |
escalated_escalation_rules |
Condition-based automation rules |
escalated_ticket_activities |
Audit log of every ticket action |
escalated_attachments |
File attachments on tickets/replies |
escalated_canned_responses |
Pre-written response templates |
escalated_macros |
Batch action definitions |
escalated_settings |
Key-value app settings |
escalated_api_tokens |
Bearer tokens for the REST API |
escalated_inbound_emails |
Processed inbound email records |
escalated_satisfaction_ratings |
CSAT ratings |
escalated_plugins |
Installed plugin registry |
escalated_plugin_store |
Per-plugin persistent key-value storage |
escalated_custom_fields |
Custom field definitions |
escalated_custom_field_values |
Custom field values on tickets |
escalated_ticket_statuses |
Custom status definitions |
escalated_business_schedules |
Business hours schedules |
escalated_holidays |
Holiday definitions |
escalated_roles |
Role definitions |
escalated_audit_logs |
System-wide audit log |
escalated_ticket_links |
Links between tickets |
escalated_side_conversations |
Side conversations on tickets |
escalated_agent_profiles |
Extended agent data (skills, capacity) |
escalated_skills |
Skill definitions for routing |
escalated_agent_capacity |
Agent workload tracking |
escalated_webhooks |
Outbound webhook configurations |
escalated_automations |
Trigger-based automation definitions |
escalated_articles |
Knowledge base articles |
escalated_article_categories |
Article categorization |
escalated_import_jobs |
Data import job tracking |
escalated_custom_objects |
Custom object schemas |
escalated_custom_object_records |
Custom object data |
escalated_saved_views |
Saved filter presets (personal or shared) |
escalated_widget_configs |
Embeddable widget configuration |
Escalated supports real-time event broadcasting as an opt-in feature. When enabled, core domain events are pushed to connected clients via WebSocket (or SSE, depending on framework).
- A domain event fires (e.g.,
TicketCreated,ReplyCreated,TicketAssigned) - The
BroadcastServicechecks if broadcasting is enabled in config - If enabled, the event is serialized and pushed to the appropriate channel/topic
- The frontend
useRealtimecomposable receives the event and updates the UI
| Framework | Transport | Library |
|---|---|---|
| Laravel | WebSocket (Pusher/Ably/Reverb) | Laravel Broadcasting + Echo |
| Rails | WebSocket | ActionCable |
| Django | WebSocket | Django Channels |
| AdonisJS | SSE | AdonisJS Transmit |
| Symfony | SSE | Mercure |
| Phoenix | WebSocket | Phoenix Channels / PubSub |
| Go | SSE | Custom goroutine-based broadcaster |
department.{id}-- All tickets in a departmentticket.{id}-- Single ticket updates (replies, status changes)agent.{id}-- Personal agent notifications (assignments, escalations)
The useRealtime composable in @escalated-dev/escalated provides a unified API. It auto-detects whether Laravel Echo is available and falls back to HTTP polling when WebSocket/SSE is not configured:
const { subscribe } = useRealtime()
subscribe(`ticket.${id}`, 'ReplyCreated', (event) => { /* update UI */ })Broadcasting is fully opt-in. When disabled:
- No WebSocket/SSE connections are established
- The frontend composable silently falls back to periodic polling
- Zero performance impact on the backend
The embeddable support widget allows customers to embed a lightweight support interface on their websites. The widget is a standalone JS bundle built from the @escalated-dev/escalated package.
Each backend framework exposes widget API endpoints (unauthenticated, CORS-enabled, rate-limited):
| Endpoint | Method | Purpose |
|---|---|---|
/support/widget/config |
GET | Widget configuration (colors, greeting, departments) |
/support/widget/articles/search |
GET | Knowledge base article search |
/support/widget/tickets |
POST | Submit a new ticket |
/support/widget/tickets/status |
GET | Look up ticket status by email + ticket number |
Admins configure the widget via admin settings:
- Accent color -- Primary brand color
- Position -- Bottom-left or bottom-right
- Greeting message -- Welcome text shown in the widget
- Departments -- Which departments are available for ticket submission
- KB search -- Whether to show KB search in the widget
<script src="https://your-app.com/support/widget/embed.js" async></script>The script loads the widget bundle and renders a floating button that opens the support panel.
Outbound notification emails now include proper email threading headers so replies thread correctly in email clients (Gmail, Outlook, Apple Mail, etc.).
- Message-ID -- Unique identifier for each outbound email, stored in
escalated_replies.message_id - In-Reply-To -- References the previous email in the thread (the message being replied to)
- References -- Full chain of Message-IDs in the conversation thread
All notification emails use branded HTML templates. Admins can configure:
- Logo URL -- Displayed in the email header
- Accent color -- Used for buttons and highlights
- Footer text -- Custom footer (company name, unsubscribe links, etc.)
Templates are rendered by each framework's native templating engine (Blade, Jinja2, ERB, Edge, Twig, EEx, Go html/template).