escalated-dev / escalated-laravel
An embeddable support ticket system for Laravel applications
Installs: 237
Dependents: 1
Suggesters: 0
Security: 0
Stars: 15
Watchers: 3
Forks: 2
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
- dev-main
- 0.6.0
- 0.5.0
- 0.4.0
- v0.1.9
- v0.1.8
- v0.1.7
- v0.1.6
- v0.1.5
- v0.1.4
- v0.1.3
- v0.1.2
- v0.1.1
- v0.1.0
- dev-fix/install-command-auto-configure-user-model
- dev-feature/api
- dev-refactor/reorganize-controllers-by-feature
- dev-feature/api-security-improvements
- dev-feature/i18n
- dev-feature/plugins
- dev-feature/fixes-and-inbound-email
This package is auto-updated.
Last update: 2026-02-18 20:36:53 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
The install command will offer to automatically configure your User model with the Ticketable interface and HasTickets trait. If you prefer to do this manually, or if you use a custom user model, add the following:
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