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

0.5.0 2026-02-11 05:54 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
});

Full events documentation →

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

  1. An external email service receives an email at your support address (e.g., support@yourapp.com)
  2. The service forwards the email to your application via webhook (or IMAP polling fetches it)
  3. Escalated normalizes the payload into an InboundMessage DTO via the adapter
  4. The InboundEmailService processes the message:
    • Thread matching: checks the subject for a ticket reference (e.g., [ESC-00001]), then checks In-Reply-To / References headers 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
  5. Every inbound email is logged to escalated_inbound_emails for 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
  1. Configure SES to receive emails and publish to an SNS topic
  2. Create an HTTPS subscription on the SNS topic pointing to:
    POST https://yourapp.com/support/inbound/ses
    
  3. 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]) and In-Reply-To / References headers
  • Guest tickets for unknown senders — display name derived from email (e.g., john.doe@example.comJohn 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-ID headers
  • Attachment handling — stores attachments respecting max_attachment_size_kb and max_attachments_per_reply
  • Auto-reopen — reopens resolved/closed tickets when a reply arrives via email
  • Audit logging — every inbound email recorded in escalated_inbound_emails with 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

Same architecture, same Vue UI, same three hosting modes — for every major backend framework.

License

MIT