escalated-dev / escalated-laravel
An embeddable support ticket system for Laravel applications
Installs: 219
Dependents: 1
Suggesters: 0
Security: 0
Stars: 14
Watchers: 3
Forks: 1
Open Issues: 1
pkg:composer/escalated-dev/escalated-laravel
Requires
- php: ^8.2
- illuminate/database: ^11.0|^12.0
- illuminate/events: ^11.0|^12.0
- illuminate/http: ^11.0|^12.0
- illuminate/notifications: ^11.0|^12.0
- illuminate/queue: ^11.0|^12.0
- illuminate/routing: ^11.0|^12.0
- illuminate/support: ^11.0|^12.0
Requires (Dev)
- inertiajs/inertia-laravel: ^2.0
- orchestra/testbench: ^9.0|^10.0
- pestphp/pest: ^2.0|^3.0
- pestphp/pest-plugin-laravel: ^2.0|^3.0
This package is auto-updated.
Last update: 2026-02-15 02:31:14 UTC
README
A full-featured, embeddable support ticket system for Laravel. Drop it into any app — get a complete helpdesk with SLA tracking, escalation rules, agent workflows, and a customer portal. No external services required.
Three hosting modes. Run entirely self-hosted, sync to a central cloud for multi-app visibility, or proxy everything to the cloud. Switch modes with a single config change.
Features
- Ticket lifecycle — Create, assign, reply, resolve, close, reopen with configurable status transitions
- SLA engine — Per-priority response and resolution targets, business hours calculation, automatic breach detection
- Escalation rules — Condition-based rules that auto-escalate, reprioritize, reassign, or notify
- Agent dashboard — Ticket queue with filters, bulk actions, internal notes, canned responses
- Customer portal — Self-service ticket creation, replies, and status tracking
- Admin panel — Manage departments, SLA policies, escalation rules, tags, and view reports
- File attachments — Drag-and-drop uploads with configurable storage and size limits
- Activity timeline — Full audit log of every action on every ticket
- Email notifications — Configurable per-event notifications with webhook support
- Department routing — Organize agents into departments with auto-assignment (round-robin)
- Tagging system — Categorize tickets with colored tags
- Guest tickets — Anonymous ticket submission with magic-link access via guest token
- Inbound email — Create and reply to tickets via email (Mailgun, Postmark, AWS SES, IMAP)
- Inertia.js + Vue 3 UI — Shared frontend via
@escalated-dev/escalated
v0.4.0 — Advanced Features
- Bulk actions — Assign, change status/priority, add tags, close, or delete multiple tickets at once
- Macros — Reusable multi-step automations (set status + assign + add note in one click)
- Ticket followers — Agents follow tickets and receive the same notifications as the assignee
- Satisfaction ratings — 1-5 star CSAT ratings with optional comments after resolution
- Pinned notes — Pin important internal notes to the top of the ticket thread
- Keyboard shortcuts — Full keyboard navigation for power users
- Quick filters — One-click filter chips (My Tickets, Unassigned, Urgent, SLA Breaching)
- Presence indicators — See who else is viewing a ticket in real-time
- Enhanced dashboard — CSAT metrics, resolution times, SLA breach tracking
Requirements
- PHP 8.2+
- Laravel 11.x or 12.x
- Node.js 18+ (for frontend assets)
Quick Start
composer require escalated-dev/escalated-laravel npm install @escalated-dev/escalated php artisan escalated:install php artisan migrate
Add the Ticketable interface to your User model:
use Escalated\Laravel\Contracts\HasTickets; use Escalated\Laravel\Contracts\Ticketable; class User extends Authenticatable implements Ticketable { use HasTickets; }
Define authorization gates in a service provider:
use Illuminate\Support\Facades\Gate; Gate::define('escalated-admin', fn ($user) => $user->is_admin); Gate::define('escalated-agent', fn ($user) => $user->is_agent || $user->is_admin);
Visit /support — you're live.
Frontend Integration
Escalated ships a Vue component library and default pages via the @escalated-dev/escalated npm package.
1. Tailwind Content
Add the Escalated package to your Tailwind content config so its classes aren't purged:
// tailwind.config.js content: [ // ... your existing paths './node_modules/@escalated-dev/escalated/src/**/*.vue', ],
2. Page Resolver
Add the Escalated page resolver to your app.ts:
import { createInertiaApp } from '@inertiajs/vue3'; import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; const escalatedPages = import.meta.glob( '../../node_modules/@escalated-dev/escalated/src/pages/**/*.vue', ); createInertiaApp({ resolve: (name) => { if (name.startsWith('Escalated/')) { const path = name.replace('Escalated/', ''); return resolvePageComponent( `../../node_modules/@escalated-dev/escalated/src/pages/${path}.vue`, escalatedPages, ); } return resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**/*.vue')); }, // ... });
3. Theming (Optional)
Register the EscalatedPlugin to render Escalated pages inside your app's layout — no page duplication needed:
import { EscalatedPlugin } from '@escalated-dev/escalated'; import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue'; createInertiaApp({ setup({ el, App, props, plugin }) { createApp({ render: () => h(App, props) }) .use(plugin) .use(EscalatedPlugin, { layout: AuthenticatedLayout, }) .mount(el); }, });
Your layout component must accept a #header slot and a default slot. Escalated will render its sub-navigation in the header and page content in the default slot.
Without the plugin, Escalated uses its own standalone layout with a simple nav bar.
CSS Custom Properties
Pass a theme option to customize colors and radii:
app.use(EscalatedPlugin, { layout: AuthenticatedLayout, theme: { primary: '#3b82f6', radius: '0.75rem', } })
| Property | Default | Description |
|---|---|---|
--esc-primary |
#4f46e5 |
Primary action color |
--esc-primary-hover |
auto-darkened | Primary hover color |
--esc-radius |
0.5rem |
Border radius for inputs and buttons |
--esc-radius-lg |
auto-scaled | Border radius for cards and panels |
--esc-font-family |
inherit | Font family override |
Available Components
| Component | Description |
|---|---|
ActivityTimeline |
Full audit log of ticket events |
AssigneeSelect |
Agent assignment dropdown |
AttachmentList |
File attachment display |
FileDropzone |
Drag-and-drop file upload |
PriorityBadge |
Priority level indicator |
ReplyComposer |
Rich text reply editor |
ReplyThread |
Chronological message thread |
SlaTimer |
SLA countdown display |
StatsCard |
Metric card for dashboards |
StatusBadge |
Ticket status indicator |
TagSelect |
Tag picker with colors |
TicketFilters |
Search and filter controls |
TicketList |
Paginated ticket table |
TicketSidebar |
Ticket metadata sidebar |
Shared Inertia Props
Escalated automatically shares data to all Inertia pages via page.props.escalated:
page.props.escalated = { prefix: 'support', // Route prefix from config is_agent: true, // Current user can access agent views is_admin: false, // Current user can access admin views }
Use these to conditionally show nav links or restrict UI elements.
Hosting Modes
Self-Hosted (default)
Everything stays in your database. No external calls. Full autonomy.
// config/escalated.php 'mode' => 'self-hosted',
Synced
Local database + automatic sync to cloud.escalated.dev for unified inbox across multiple apps. If the cloud is unreachable, your app keeps working — events queue and retry.
'mode' => 'synced', 'hosted' => [ 'api_url' => 'https://cloud.escalated.dev/api/v1', 'api_key' => env('ESCALATED_API_KEY'), ],
Cloud
All ticket data proxied to the cloud API. Your app handles auth and renders UI, but storage lives in the cloud. Supports multiple domains per API key.
'mode' => 'cloud',
All three modes share the same controllers, UI, and business logic. The driver pattern handles the rest.
Publishing Assets
# Email templates php artisan vendor:publish --tag=escalated-views # Config file php artisan vendor:publish --tag=escalated-config # Database migrations php artisan vendor:publish --tag=escalated-migrations
Scheduling
Add these to your scheduler for SLA and escalation automation:
// app/Console/Kernel.php or routes/console.php Schedule::command('escalated:check-sla')->everyMinute(); Schedule::command('escalated:evaluate-escalations')->everyFiveMinutes(); Schedule::command('escalated:close-resolved')->daily(); Schedule::command('escalated:purge-activities')->weekly(); Schedule::command('escalated:poll-imap')->everyMinute(); // Only if using IMAP adapter
Configuration
All config lives in config/escalated.php. Key options:
'mode' => 'self-hosted', // self-hosted | synced | cloud 'user_model' => App\Models\User::class, 'table_prefix' => 'escalated_', 'default_priority' => 'medium', 'routes' => [ 'prefix' => 'support', 'middleware' => ['web', 'auth'], ], 'tickets' => [ 'allow_customer_close' => true, 'auto_close_resolved_after_days' => 7, ], 'sla' => [ 'enabled' => true, 'business_hours_only' => false, 'business_hours' => [ 'start' => '09:00', 'end' => '17:00', 'timezone' => 'UTC', 'days' => [1, 2, 3, 4, 5], ], ],
See the full configuration reference.
Events
Every ticket action dispatches an event you can listen to:
| Event | When |
|---|---|
TicketCreated |
New ticket |
TicketStatusChanged |
Status transition |
TicketAssigned |
Agent assigned |
ReplyCreated |
Public reply added |
InternalNoteAdded |
Agent note added |
SlaBreached |
SLA deadline missed |
TicketEscalated |
Ticket escalated |
TicketResolved |
Ticket resolved |
TicketClosed |
Ticket closed |
use Escalated\Laravel\Events\TicketCreated; Event::listen(TicketCreated::class, function ($event) { // $event->ticket });
Inbound Email
Escalated can create and reply to tickets from incoming emails. Supports Mailgun, Postmark, AWS SES webhooks, and IMAP polling as a fallback.
How It Works
- An external email service receives an email at your support address (e.g.,
support@yourapp.com) - The service forwards the email to your application via webhook (or IMAP polling fetches it)
- Escalated normalizes the payload into an
InboundMessageDTO via the adapter - The
InboundEmailServiceprocesses the message:- Thread matching: checks the subject for a ticket reference (e.g.,
[ESC-00001]), then checksIn-Reply-To/Referencesheaders against stored message IDs - Match found: adds a reply to the existing ticket; reopens the ticket if it was resolved or closed
- No match: creates a new ticket — if the sender is a registered user they become the requester, otherwise a guest ticket is created
- Thread matching: checks the subject for a ticket reference (e.g.,
- Every inbound email is logged to
escalated_inbound_emailsfor audit
Enable Inbound Email
ESCALATED_INBOUND_EMAIL=true ESCALATED_INBOUND_ADDRESS=support@yourapp.com
Adapter Setup
Mailgun
ESCALATED_INBOUND_ADAPTER=mailgun ESCALATED_MAILGUN_SIGNING_KEY=your-mailgun-signing-key
Configure a Mailgun Route to forward inbound emails to:
POST https://yourapp.com/support/inbound/mailgun
The signing key is in your Mailgun dashboard under Settings > API Keys > HTTP Webhook Signing Key. Requests are verified via HMAC-SHA256 signature.
Postmark
ESCALATED_INBOUND_ADAPTER=postmark ESCALATED_POSTMARK_INBOUND_TOKEN=your-postmark-inbound-token
Configure an Inbound Webhook in your Postmark server settings pointing to:
POST https://yourapp.com/support/inbound/postmark
The token is sent in the X-Postmark-Token header and verified on each request.
AWS SES
ESCALATED_INBOUND_ADAPTER=ses ESCALATED_SES_REGION=us-east-1 ESCALATED_SES_TOPIC_ARN=arn:aws:sns:us-east-1:123456789:your-topic
- Configure SES to receive emails and publish to an SNS topic
- Create an HTTPS subscription on the SNS topic pointing to:
POST https://yourapp.com/support/inbound/ses - Escalated auto-confirms the SNS subscription and verifies message signatures using Amazon's certificate
IMAP (Fallback)
For providers without webhook support, poll via IMAP:
ESCALATED_INBOUND_ADAPTER=imap ESCALATED_IMAP_HOST=imap.gmail.com ESCALATED_IMAP_PORT=993 ESCALATED_IMAP_ENCRYPTION=ssl ESCALATED_IMAP_USERNAME=support@yourapp.com ESCALATED_IMAP_PASSWORD=your-app-password ESCALATED_IMAP_MAILBOX=INBOX
Schedule the poll command:
Schedule::command('escalated:poll-imap')->everyMinute();
Webhook URL
POST /{prefix}/inbound/{adapter}
Where {prefix} is your configured route prefix (default: support) and {adapter} is mailgun, postmark, or ses. These routes use the api middleware (no CSRF, no auth).
Processing Features
- Thread detection via subject reference pattern (
[ESC-00001]) andIn-Reply-To/Referencesheaders - Guest tickets for unknown senders — display name derived from email (e.g.,
john.doe@example.com→John Doe) - Subject sanitization — strips
RE:,FW:,FWD:prefixes (including stacked) - HTML fallback — uses stripped HTML body when plain text is empty
- Duplicate detection — skips messages with duplicate
Message-IDheaders - Attachment handling — stores attachments respecting
max_attachment_size_kbandmax_attachments_per_reply - Auto-reopen — reopens resolved/closed tickets when a reply arrives via email
- Audit logging — every inbound email recorded in
escalated_inbound_emailswith status tracking
Custom Adapter
Implement the InboundAdapter interface:
use Escalated\Laravel\Mail\Adapters\InboundAdapter; use Escalated\Laravel\Mail\InboundMessage; use Illuminate\Http\Request; class MyAdapter implements InboundAdapter { public function parseRequest(Request $request): InboundMessage { return new InboundMessage( fromEmail: $request->input('from'), fromName: $request->input('name'), toEmail: $request->input('to'), subject: $request->input('subject'), bodyText: $request->input('text'), bodyHtml: $request->input('html'), messageId: $request->input('message_id'), inReplyTo: $request->input('in_reply_to'), ); } public function verifyRequest(Request $request): bool { return $request->header('X-Secret') === config('services.my_adapter.secret'); } }
Inbound Email Environment Variables
| Variable | Default | Description |
|---|---|---|
ESCALATED_INBOUND_EMAIL |
false |
Enable inbound email |
ESCALATED_INBOUND_ADAPTER |
mailgun |
Default adapter |
ESCALATED_INBOUND_ADDRESS |
support@example.com |
Support email address |
ESCALATED_MAILGUN_SIGNING_KEY |
— | Mailgun webhook signing key |
ESCALATED_POSTMARK_INBOUND_TOKEN |
— | Postmark inbound token |
ESCALATED_SES_REGION |
us-east-1 |
AWS SES region |
ESCALATED_SES_TOPIC_ARN |
— | AWS SNS topic ARN |
ESCALATED_IMAP_HOST |
— | IMAP server hostname |
ESCALATED_IMAP_PORT |
993 |
IMAP server port |
ESCALATED_IMAP_ENCRYPTION |
ssl |
IMAP encryption |
ESCALATED_IMAP_USERNAME |
— | IMAP username |
ESCALATED_IMAP_PASSWORD |
— | IMAP password |
ESCALATED_IMAP_MAILBOX |
INBOX |
IMAP mailbox to poll |
Routes
| Route | Method | Description |
|---|---|---|
/support |
GET | Customer ticket list |
/support/create |
GET | New ticket form |
/support/{ticket} |
GET | Ticket detail |
/support/guest/create |
GET | Guest ticket form |
/support/guest/{token} |
GET | Guest ticket view (magic link) |
/support/agent |
GET | Agent dashboard |
/support/agent/tickets |
GET | Agent ticket queue |
/support/agent/tickets/{ticket} |
GET | Agent ticket view |
/support/admin/reports |
GET | Admin reports |
/support/admin/departments |
GET | Department management |
/support/admin/sla-policies |
GET | SLA policy management |
/support/admin/escalation-rules |
GET | Escalation rule management |
/support/admin/tags |
GET | Tag management |
/support/admin/canned-responses |
GET | Canned response management |
/support/inbound/mailgun |
POST | Mailgun inbound webhook |
/support/inbound/postmark |
POST | Postmark inbound webhook |
/support/inbound/ses |
POST | SES/SNS inbound webhook |
/support/agent/tickets/bulk |
POST | Bulk actions on multiple tickets |
/support/agent/tickets/{ticket}/follow |
POST | Follow/unfollow a ticket |
/support/agent/tickets/{ticket}/macro |
POST | Apply a macro to a ticket |
/support/agent/tickets/{ticket}/presence |
POST | Update presence on a ticket |
/support/agent/tickets/{ticket}/pin/{reply} |
POST | Pin/unpin an internal note |
/support/{ticket}/rate |
POST | Submit satisfaction rating |
All routes use the configurable prefix (default: support). Inbound webhook routes use the api middleware (no auth, no CSRF).
Documentation
Testing
composer install vendor/bin/pest
Also Available For
- Escalated for Laravel — Laravel Composer package (you are here)
- Escalated for Rails — Ruby on Rails engine
- Escalated for Django — Django reusable app
- Escalated for AdonisJS — AdonisJS v6 package
- Escalated for Filament — Filament v3 admin panel plugin
- Shared Frontend — Vue 3 + Inertia.js UI components
Same architecture, same Vue UI, same three hosting modes — for every major backend framework.
License
MIT