escalated-dev/escalated-laravel
A full-featured, embeddable support ticket system for Laravel with SLA tracking, escalation rules, agent workflows, and a customer portal. Self-host or sync to cloud with one config change.
escalated.widget.allowed_origins (ESCALATED_WIDGET_ALLOWED_ORIGINS) or the new Sites that can embed the widget admin setting (widget_allowed_origins); https://*.example.com subdomain wildcards are supported. Widget requests from a listed origin get CORS headers (including on 422 and 429 answers), skip the CSRF check and always run as a guest. Requests from any other origin are handled exactly as before. Empty by default, so nothing changes until a site is listed. See docs/embedding-the-widget.md.GET {prefix}/widget/embed.js serves the standalone widget bundle built from [@escalated-dev](https://github.com/escalated-dev)/escalated, so other sites need one script tag. escalated.widget.serve_script turns the route off.metadata (order number, plan, page URL, ...), sanitised to a flat map of short scalar values, plus a server-recorded embedded_on origin. /config advertises accepts_metadata: true.widget_enabled, colour, position, greeting, departments). The controller dropped them, so the widget could not be switched on from the UI.metadata from the widget was stored unbounded; it is now sanitised like ticket metadata.actions_executed as a count, as the NestJS reference sends
it; this package passed the stored array through. WorkflowController::logs
now sends the count, with the list still in action_details. (#206)[@escalated-dev](https://github.com/escalated-dev)/escalated ^0.11.9, whose admin
and agent panels work on narrow screens: below 1024px the admin sidebar folds
into a drawer behind a menu button, the agent nav folds into a menu, and
tables scroll inside their cards instead of widening the page. It also adds
optional panel theme hooks (activeText, headerBg, headerText,
logoTileBg, logoTileFg), set in the host's EscalatedPlugin options. No
backend change is needed; the page-name fixture is refreshed to 0.11.9.Patch release on top of v1.5.0 (the newsletter system release).
laravel/framework 11.x security advisories that Composer 2.9+ excludes during dependency resolution, which was breaking the Laravel 11 compatibility test matrix. Scoped to this package's root config.audit.ignore — does not propagate to host apps. No runtime/code changes.See v1.5.0 for the newsletter system, add_follower, ticket subjects, and permission enforcement.
escalated:newsletters:dispatch scheduled command (per-minute rate limiting, retry backoff, auto-pause on high bounce rates). Disabled by default behind escalated.enable_newsletters. (#103, #128)add_follower workflow action — auto-subscribe a host user as a ticket follower from a workflow rule. (#127)escalated.permissions is now shared with the Inertia frontend for per-permission nav gating. (#129)newsletters.manage (all actions) and newsletters.send (send/test). Previously seeded but unchecked. (#129)Full changelog: https://github.com/escalated-dev/escalated-laravel/blob/main/CHANGELOG.md
Patch release fixing upgrade-safety regressions found in a breaking-change audit of v1.4.0.
routing_tag_ids/routing_department_ids skill columns by editing the original create_escalated_skills_table migration, which never re-runs on an existing install — so apps upgraded from an earlier version were missing the columns and every skill create/edit failed (Skill::saving() always writes them). Adds a Schema::hasColumn-guarded backfill migration (no-op on fresh installs).AssignTicketRequest (Admin/Agent assign endpoints) again validates that agent_id exists, so unknown/garbage input returns a clean 422 instead of a 500. UUID/string keys are still accepted.AssignmentService::__construct() $skillRoutingService is now optional (container fallback), restoring the v1.3.0 new AssignmentService($manager) single-argument signature.int → int|string widening for UUID/string user-key support touches the public TicketDriver::assignTicket() contract and Ticket methods (assign, follow, unfollow, isFollowedBy, scopeAssignedTo). If you implement TicketDriver or subclass Ticket with int-typed parameters, widen them to int|string to avoid a PHP "must be compatible" fatal. Callers are unaffected.TicketAssigned::$agentId is now int|string (a string for UUID/string-keyed apps). Integer-keyed apps are unchanged.Full changelog: https://github.com/escalated-dev/escalated-laravel/compare/v1.4.0...v1.4.1
UUID / string host user keys — Escalated for Laravel now works with host apps whose user primary key is a UUID, ULID, or string, not just an integer. SavedView::scopeForUser() and every other user-id parameter accept int|string (fixing a TypeError 500 on /support/admin/tickets for non-integer-keyed apps), user ids are never cast to int (which corrupted UUIDs), and the package's user-referencing migration columns are auto-typed to match your user_model via the new escalated.user_key_type config (auto by default). Integer-keyed apps are unaffected.
Custom Ticket Actions — register host-defined agent ticket buttons that dispatch a TicketCustomActionTriggered event (with an audit note) when clicked.
escalated.user_key_type (#112)escalated-dev/locale translation consumptionqs 6.15.1 → 6.15.2 in the demo host-app (CVE-2026-8723) (#113)Full changelog: https://github.com/escalated-dev/escalated-laravel/compare/v1.3.0...v1.4.0
Admin → Users page (#94): new Admin/UserController exposes the host User table (paged, searchable) plus a PATCH /admin/users/{user}/role endpoint that flips one role at a time. Companion Vue page lands in [email protected]. Surfaced from escalated#60, where the reporter asked "how do you create agent?" — admins no longer have to drop into tinker to grant or revoke staff access.
Demoting yourself from admin is rejected server-side so an admin cannot lock themselves out of the panel they're using. Demoting an admin via the agent toggle revokes both flags in one step.
The default install pins this to the is_admin / is_agent columns the install command tells hosts to add. Hosts wiring the gates differently (Spatie roles, custom pivots) should override Admin/UserController in their own routes — there's a comment to that effect in the class.
composer update escalated-dev/escalated-laravel
No new migrations. The Users nav entry shows up automatically once you upgrade [@escalated-dev](https://github.com/escalated-dev)/escalated to 0.8.0.
Full Changelog: https://github.com/escalated-dev/escalated-laravel/compare/v1.2.5...v1.3.0
Resolves the install failure reported in #88 for real this time (#92)
The actual underlying issue (surfaced thanks to verbose migrate output): MariaDB and older MySQL versions reject escalated_macros's foreign key to users(id) with errno: 150 "Foreign key constraint is incorrectly formed" whenever the host app's users table doesn't exactly match what we expect. Reproduced on MariaDB 11 under four common host configurations:
users table on MyISAM (legacy Laragon / WAMP default_storage_engine=MyISAM)users.id is signed BIGINT (project upgraded from Laravel pre-5.8)users.id is INT UNSIGNED (project from pre-5.8 increments())users.id is CHAR(36) UUID (custom user model with HasUuids)We can't control any of those four from a package, so this release drops the foreign-key constraints from the five migrations that pointed at the host's users table:
escalated_macros.created_byescalated_ticket_followers.user_idescalated_saved_views.user_idescalated_mentions.user_idescalated_tickets.snoozed_byEach becomes a plain unsignedBigInteger column. Foreign keys to our own tables (e.g. escalated_tickets) stay — those always work because we own both sides.
This is the same pattern spatie/laravel-permission, spatie/laravel-activitylog, and Filament use for the same reason.
escalated:install was using callSilently('migrate') wrapped in a task component, which swallowed Laravel's actual SQL error and just printed FAIL. Now it prints the full migrate output (per-migration timing, failing migration name, full PDO/SQL error). Same treatment for db:seed. Took a back-and-forth with the reporter to get the real error before — won't happen again.
Forward-compatible only. Anyone with a successful prior install keeps their existing FK constraints because Laravel doesn't re-run migrations once recorded. Anyone hit by #88 sees migrations 14+ run cleanly on their next php artisan migrate.
If a host app hard-deletes users, orphaned created_by / user_id rows in our tables won't be cascade-cleaned anymore (most apps soft-delete users so this is a non-issue in practice). If you need cleanup, attach a User-deleting model observer in the host app.
composer update escalated-dev/escalated-laravel
php artisan escalated:install
Full Changelog: https://github.com/escalated-dev/escalated-laravel/compare/v1.2.4...v1.2.5
Install translations now actually flow from escalated-dev/locale (#90)
The central escalated-dev/locale package was already declared as a composer dep but wasn't actually serving any translations — its JSON uses camelCase keys (matching the Vue frontend) while every PHP-side __() call used snake_case, so the central package was effectively dead weight and translations came entirely from the package's own resources/lang/ PHP fallback.
This release wires the install command to actually pull from central:
EscalatedServiceProvider (bridgeCentralGroups()) reads each <locale>.json from escalated-dev/locale and injects the nested data into Laravel's translator under the escalated namespace via addLines(). After the bridge fires, __('escalated::commands.install.publishingConfig') resolves against the central JSON. The PHP fallback's commands.php is no longer loaded for the commands group in production — central is the single source of truth.{placeholder} syntax (matches the Vue t() helper) to Laravel's :placeholder convention on the way in, so call sites stay idiomatic.escalated::commands.install.* keys converted from snake_case to camelCase in InstallCommand.php and in every locale's commands.php fallback. The fallback now serves as a parity-with-central offline backup for the dev path.escalated-dev/locale: ^0.1.8 so the new install keys (seedingPermissions, runningMigrations, runMigrationsConfirm, stepSeed from escalated-locale v0.1.8) are guaranteed present.Only the commands group is bridged today. Other groups — enums, notifications, emails, messages, validation — continue to load from resources/lang/ because their snake_case enum-value keys (e.g. enums.status.in_progress) don't yet have camelCase parity in central. Migrating those is a separate follow-up since enum values are persisted in user databases.
resources/lang/ PHP files (now camelCase, matching the call sites).lang/vendor/escalated/{locale}/commands.php still work via Laravel's loadNamespaceOverrides mechanism (unchanged).tests/Unit/CentralLocaleBridgeTest.php (3 tests, 23 assertions) locks in the bridge: populates $loaded['escalated']['commands'], resolved values match central JSON exactly across en and fr, and {placeholder} → :placeholder rewriting works.Full Changelog: https://github.com/escalated-dev/escalated-laravel/compare/v1.2.3...v1.2.4
escalated:install no longer breaks on a clean install (#89, resolves #88)
Previously the command published migration files and then immediately tried to seed escalated_permissions — a table that hadn't been created yet, causing every fresh install to fail with SQLSTATE[42S02] Table doesn't exist. The command now prompts to run migrations + seed default permissions in the correct order, and the printed setup instructions are state-aware (they drop the migrate / seed steps when the user opted into auto-running them).
Also adds the missing seeding_permissions translation key across all 14 locales, fixes a __() call that was silently passing a fallback string into the $locale slot, and adds new running_migrations, run_migrations_confirm, step_seed keys for the new prompts.
delayed_actions index (#87) — pre-empts the Identifier name too long MySQL error for users with a custom escalated.table_prefix longer than 13 characters.ticket_links unique index (#86, thanks @matalaweb) — fixes the same class of bug for the default escalated_ prefix on MySQL/PostgreSQL.[@escalated-dev](https://github.com/escalated-dev)/locale (#85)All changes in this release are backwards-compatible. The migration index renames in #86 / #87 only take effect on fresh installs — anyone who already ran the original migrations keeps the old auto-generated index names (Laravel doesn't re-run migrations).
Full Changelog: https://github.com/escalated-dev/escalated-laravel/compare/v1.2.2...v1.2.3
Customer priority filter now actually filters (#66, fixes #64) — Customer\TicketController@index was dropping the priority URL param (and several other visible filters) before calling the driver. The shared TicketFilters.vue component still rendered a priority dropdown, so the param landed in the URL but never reached a where clause. Allow-list now matches every key the UI sends: status, priority, ticket_type, search, tag, has_attachments, created_after, created_before, plus sort controls. Agent/admin/api paths were never affected — regression tests added for all three to prevent drift.
Graceful fallback when the host's users table has no name column (#65, fixes #63) — ticket search (Ticket::scopeSearch + LocalDriver requester filter) assumed a name column existed on the host's user model and would either error against Postgres (column users.name does not exist) or silently return empty against sqlite. The new Escalated::applyUserSearch() helper now consults config('escalated.user_display_column'), checks Schema::hasColumn once per process, and silently degrades to email-only search when the column doesn't exist. Hosts with split first_name/last_name columns now work out of the box; setting ESCALATED_USER_DISPLAY_COLUMN=first_name gives them full first_name+email search.
Install command no longer duplicates migrations (#62, fixes #61) — re-running php artisan escalated:install (e.g. during package upgrades) previously republished every migration with a fresh timestamp prefix, leaving the host with duplicate *_create_escalated_*_table.php files and a broken migrate run. Install now globs for existing *_create_escalated_*_table.php files and skips publication if any are present; --force deletes the stale copies first before republishing.
Full changelog: https://github.com/escalated-dev/escalated-laravel/compare/v1.2.1...v1.2.2
ReportingService and ReportController::avgFirstResponseHours previously emitted MySQL-only TIMESTAMPDIFF(HOUR, …) and DATE_FORMAT(…) SQL, which Postgres rejects with column "hour" does not exist. Helpers now match on the driver across sqlite | pgsql | mysql and emit EXTRACT(EPOCH FROM (to - from)) / 3600 / to_char(…, 'YYYY-MM') etc. on Postgres. 16 new unit tests assert exact SQL per driver.docker/ (#58). docker compose up --build from docker/ boots a 3-container Postgres-backed Laravel host (PHP 8.3 + Postgres 16 + Mailpit) with the package installed and a /demo click-to-login picker. Excluded from the Composer dist via archive.exclude — won't appear in vendor/.Escalated\Laravel\Database\Factories\ to the production PSR-4 autoload so Model::factory() resolves at runtime in real installs (same class of bug as #55).Full changelog: https://github.com/escalated-dev/escalated-laravel/compare/v1.2.0...v1.2.1
WorkflowEngine::actionSendWebhook() now validates URL scheme (http/https only) and rejects URLs that resolve to private/reserved IPs (#49)compareValues() matches operator via safeRegexMatch() with pattern validation and a PCRE backtrack limit of 10,000 (#49)in: validation for actions.*.type in WorkflowController store/update (#49)resolveFieldValue() default case now whitelists subject, description, ticket_type, channel instead of open $ticket->{$field} access (#49)AuditLog entries (#49)php artisan escalated:install no longer aborts with Target class [Escalated\Laravel\Database\Seeders\PermissionSeeder] does not exist — the seeder namespace is now registered in production autoload (#56, fixes #55)url (#50)$appends so list endpoints stay fast; now only included in detail serialization (#53)minimum-stability set to stable and audit.ignore added for two phpunit advisories that were preventing the resolver from selecting any compatible phpunit version (#57)Full changelog: https://github.com/escalated-dev/escalated-laravel/compare/v1.1.0...v1.2.0
Ticket model scopeTicketCreated/TicketUpdated via model hooks, ReplyCreated/InternalNoteAdded via Reply::booted()LogTicketStatusChange listenerTicketCreated event timing — listeners now receive the final ESC-XXXXX reference, not TEMP-{uuid}InternalNoteAddedmarkEscalated() incorrectly setting resolved_atassign() and changeDepartment() error messagesTicketPriority::from() cast in inbound email guest ticket creationLogTicketStatusChange listener in service providerNotifiable trait to HasTicketspackage-lock.jsonThe first stable release of Escalated for Laravel — a full-featured, embeddable support ticket system.
artisan escalated:plugins for discovering pluginscomposer require escalated-dev/escalated-laravel:^1.0
php artisan migrate
php artisan escalated:install --force
Review the new config options in config/escalated.php after upgrading.
Full Changelog: https://github.com/escalated-dev/escalated-laravel/compare/v0.6.0...v1.0.0
How can I help you explore Laravel packages today?