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.
Package shape. escalated-dev/escalated-laravel is a full helpdesk (tickets, SLA engine, escalation rules, agent queue, admin panel, customer portal, KB, embeddable widget, inbound email) delivered as an installable Laravel package plus a companion Vue 3 component library published separately as @escalated-dev/escalated. It is not a thin SDK — it is an application shipped inside a package, with its own routes, migrations, config, gates, views, Artisan commands, and frontend build coupling.
Architecture fit. Fit is good but narrow: Laravel 11–13, PHP 8.2+, and — critically — an Inertia.js + Vue 3 + Tailwind host application. The integration model is:
composer require → php artisan escalated:install → php artisan migrateUser model implements Ticketable / HasTickets (installer can auto-patch it)escalated-admin, escalated-agent) defined by the host appapp.ts (Inertia page resolver namespace Escalated/), Tailwind content/@source paths, and optionally EscalatedPlugin for host-layout theming/support by default)If the host app is Blade/Livewire, a React SPA, or API-only, the value proposition largely evaporates — you would be writing your own UI against its controllers. That is the single biggest architectural gate.
Notable design strengths. Genuinely mature for a 28-star package:
user_key_type override — removes a common integration failure.self-hosted, synced, cloud) behind a driver pattern, with the same controllers/UI across modes; synced degrades to queued retry rather than hard failure.TicketSubject contract for attaching arbitrary host models (Project, Customer, asset) with an explicit allowlist for the agent API — a correct, security-aware default.In-Reply-To/References, branded templates, inbound email across Mailgun/Postmark/SES/IMAP.escalated:wake-snoozed-tickets implies scheduler-driven state machine — operationally sane, but a hard runtime dependency.Changes in v1.9.4 (assessed). This is a feature release with two security-relevant surface changes and two fixes:
escalated.widget.allowed_origins / ESCALATED_WIDGET_ALLOWED_ORIGINS, mirrored as an admin setting widget_allowed_origins) now grants listed origins CORS headers and a CSRF-check exemption, with requests forced to run as guests. Wildcard subdomains (https://*.example.com) are supported. Empty by default.GET {prefix}/widget/embed.js serves a widget bundle built from the npm companion package; disable via escalated.widget.serve_script.embedded_on origin. /config advertises accepts_metadata: true — a new client-facing contract.metadata now sanitised like ticket metadata (previously unbounded — a prior storage-abuse/PII defect).Net effect: the package is now a first-class embeddable product rather than a self-contained one, and the widget is a supported public ingress path with a documented configuration contract. The default-empty allowlist means the posture is unchanged until opted into — but the decision to opt in is now a discrete, reviewable risk decision rather than an incidental one.
Technical risk.
| Risk | Severity | Note |
|---|---|---|
| Invasive schema ownership | High | Many host-DB tables created by package migrations, key-typed by reflection at migrate time. Type is chosen once; changing user key type later "requires a manual migration" per README. Treat as a one-way door. |
| Frontend lock-in | High | Vue 3 + Inertia + Tailwind coupling, plus a second package (@escalated-dev/escalated) that must be version-locked in step. Two release trains. |
| Widget ingress (deliberate CSRF exemption) | High (raised from Medium) | Listed origins skip CSRF and run as guests. Correct for a cross-site embed, but it means the origin allowlist is now a security boundary owned by whoever can edit config or the admin settings page. Wildcard matching must be verified in code, not assumed (https://*.example.com must not match https://evil.com/?x=.example.com or https://*.example.com.attacker.net). |
| Widget bundle served by the host app | Medium (new) | widget/embed.js ships a JS bundle built from the npm package and served by PHP. Any widget-side fix now requires a PHP package release, and a PHP-only bump can serve a bundle mismatched to the installed @escalated-dev/escalated. Confirm the shipped artifact is version-stamped and integrity-checkable. |
| Metadata PII capture | Medium (new) | metadata + embedded_on record page URL, plan, order number and the embedding origin on customer-visible records. Sanitisation is described as flat/short scalars but size caps, retention, and export/erasure behaviour are unstated. |
| Adoption signal | High | 28 stars, 0 dependents, single-vendor. No third-party evidence of production use, no community answers for edge cases. |
| Release chronology | Medium | Last release listed as 2026-10-19 — verify this; if it is a data artifact rather than a real tag, re-baseline stability expectations. |
| Attack surface (broader) | Medium–High | Guest magic-link tickets, an embeddable <script> widget now explicitly cross-origin, inbound email webhooks, and file uploads all widen exposure. The 1.9.4 notes reference 422/429 answers from widget endpoints, implying rate limiting exists — verify limits, token TTL, and webhook signature checks in code. |
| Authorization granularity | Medium | Two boolean-style gates only. No per-department or per-queue scoping described; RBAC depth is likely thinner than enterprise expectations. |
| Cloud mode data egress | Medium | synced/cloud ship ticket content (customer PII) to cloud.escalated.dev — now including widget metadata. Requires security/DPA review before any use. |
Key questions for the maintainer / spike.
http vs https) and port part of the match? Is the stored admin setting validated against the same rules as env config, and who can change it?escalated-agent / escalated-admin gates support scoping to departments, or is authorization effectively global once granted?Verdict. Feasible and unusually complete for its maturity level; the risk is not technical feasibility but concentration — of stack assumptions, of schema ownership, and of vendor dependency. v1.9.4 raises capability and raises the security-review bar in the same step: the widget is now a supported cross-origin public ingress with a config-managed allowlist, and the release notes confirm a widget-enablement defect existed up to this point, so any prior evaluation of widget behaviour should be re-baselined. Appropriate for a time-boxed spike with a documented exit path, with the embed feature treated as a separate, later go/no-go — not for an unexamined production commitment.
Stack fit assessment. Score fit before writing code:
| Host stack | Fit | Comment |
|---|---|---|
| Laravel 11–13 + Inertia + Vue 3 + Tailwind | Strong | Designed path; near-drop-in |
| Laravel + Inertia + React/Svelte | Weak | Page resolver and component library are Vue-only; you inherit backend, rebuild frontend |
| Laravel + Livewire/Blade | Weak | Would need custom views over package controllers; most delivered value lost |
| Laravel API-only / headless | Moderate | Viable if you only want the domain layer (tickets, SLA, escalation) — but that is not the advertised product |
| Non-Laravel marketing site / docs site as embed host | New in 1.9.4 | A listed origin needs only one <script> tag; the host site's stack is irrelevant |
Migration path (recommended, 3-day time-box).
escalated-dev/escalated-laravel and @escalated-dev/escalated. Do not integrate on main.--tag=escalated-config, --tag=escalated-migrations) and review the generated SQL before running against any shared database. Confirm the reflected user-key type matches your model. Verify table-name collisions against existing tables. If upgrading an existing install, diff for new columns/tables carrying widget metadata and embedded_on.User model's admin/agent flags exist and map cleanly to the two gates. If agent status is derived (roles, teams), write the gate closures explicitly rather than bolting booleans onto the users table.content/@source (v3 vs v4 paths differ per README), app.ts page resolver, EscalatedPlugin with your AuthenticatedLayout (must accept #header and default slots), theme via CSS custom properties.escalated:wake-snoozed-tickets and SLA breach detection; queue worker for notifications, sync retries, inbound mail; storage disk config for attachments.widget.enabled=false and widget.allowed_origins empty for the first sprint. When you do opt in: enumerate origins explicitly (no wildcards in the first cut), set serve_script intentionally, and confirm the admin-settings path and the env path cannot disagree. Because pre-1.9.4 admin saves silently dropped widget settings, verify persistence after save rather than trusting the UI.self-hosted for the spike. Treat synced/cloud as a separate, later, security-reviewed decision — cloud egress now includes widget metadata.Compatibility checklist. Laravel constraint vs host; PHP 8.2+; Node 18+; Inertia version; Tailwind major version; Vite; DB driver (verify the user-key reflection behaves on your driver, particularly Postgres UUIDs); mail provider webhook availability for inbound; broadcasting credentials if opting into Reverb/Pusher/Soketi; and — new — CORS behaviour through any reverse proxy/CDN in front of the app, since the widget's cross-origin path is now load-bearing.
Sequencing guidance. Land the backend + schema first and validate it headlessly (routes, policies, ticket lifecycle via artisan/tinker) before touching the frontend build. Frontend issues (Tailwind purging, page resolver paths) are noisy and will mask domain-layer problems if tackled simultaneously. Ship internal-only (agents only, portal disabled, widget not embedded) for one sprint before exposing the customer portal — that isolates auth and SLA correctness from public attack surface. Add the cross-origin widget only after the same-origin portal is stable, so CORS/CSRF failures are not confused with portal bugs.
Rollback posture. Establish before commit: which tables the package owns, whether migrate:rollback is safe, and whether a feature-flag on the route prefix (escalated.prefix) is sufficient to dark-launch. For the widget specifically, confirm that emptying allowed_origins (or setting serve_script=false) fully reverts the exposure — a config-only kill switch is the desired property, but verify it also removes the embed.js route rather than merely 404-ing requests. If uninstall is not clean, that is a decision-changing finding, not a footnote.
Maintenance. You are adopting a product, not a library. Post-integration you own: schema migrations inside your app's migration history, a Vue component library that upgrades on a separate cadence, a scheduler-driven SLA state machine, queue workers, file storage, outbound email deliverability, and (if enabled) inbound mail webhooks plus a publicly served JS bundle. Two version trains must move in lockstep — a PHP bump without the matching npm bump is a realistic breakage, and after 1.9.4 that mismatch can ship through widget/embed.js to third-party sites you do not deploy to. Treat "upgrade PHP package" as "upgrade PHP package and re-verify the served bundle version." Budget ongoing maintenance; this is not "install and forget."
Support. Single-vendor, low-adoption package. Expect to be the first to hit edge cases: no StackOverflow corpus, no third-party blog posts, no dependents to inspect for patterns. Two newly demonstrated classes of defect reinforce this — widget settings silently not persisting from the admin UI, and unbounded metadata storage — both of which were only corrected in 1.9.4, i.e. they were live for prior adopters without a visible failure signal. The commercial escalated.dev offering is the likely escalation path — confirm support terms before depending on it. Preserve the ability to fork: MIT license makes that viable, but forking a full application (now including a widget bundle) is a large commitment, so treat it as a last resort rather than the plan.
Scaling. Four axes to watch:
metadata grow monotonically. Confirm indexes on tickets (requester, assignee, status, SLA due) and plan attachment offload to object storage early. If widget traffic is high-volume, metadata adds a small but non-zero growth multiplier; confirm caps empirically./config and chat endpoints.Failure modes and detection.
| Failure | Symptom | Mitigation |
|---|---|---|
| Scheduler not running | SLA breaches never fire, snoozed tickets never wake — silent | Alert on escalated:wake-snoozed-tickets heartbeat and on overdue-but-unbreached ticket count |
| Queue worker down | Notifications and cloud sync stop; UI still works | Queue-depth and failed-job alerts |
Cloud unreachable (synced) |
Local works, unified inbox diverges | Monitor sync backlog; document that divergence is expected and self-healing on retry |
| Inbound email webhook failure | Replies lost with no user-visible error | Verify signatures, alert on webhook 4xx/5xx, reconcile against provider logs |
| User-key type mismatch | FK/join errors or truncated UUIDs after migration | Validate reflected column types in staging before production migrate |
| Tailwind purge / resolver path errors | Unstyled pages or "page not found" in Inertia | Add a smoke test hitting each Escalated route in CI |
| Gate misconfiguration | Customers reach agent views | Automated authorization test per role, run on every deploy |
| Origin allowlist misconfiguration (new) | Embedded widget silently fails (CORS) or over-permissive wildcard admits unintended sites | Load-test the origin matrix in CI; reject wildcards in review; log and alert on widget requests with unmatched Origin |
embed.js route disabled or mismatched bundle (new) |
Third-party embeds break with no host-app error | Monitor 404s on {prefix}/widget/embed.js; stamp and assert bundle version at deploy |
| Admin setting overwritten by config (new) | Widget silently disabled after a deploy, or allowlist reverts | Decide single source of truth (env vs admin) and treat the admin UI as view-only if env governs |
| Metadata leakage (new) | Page URLs or order numbers appear in exports/logs beyond intended audience | Review sanitisation caps in code; include metadata in data-retention and erasure runbooks |
Ramp-up. Engineering ramp is fast for anyone fluent in Laravel + Inertia (an afternoon to read the surface, a day to wire it). The real ramp is operational and procedural: agents must learn queue semantics, SLA policy intent, escalation rule precedence, and status-transition constraints. Budget for configuring departments, SLA policies, and escalation rules deliberately — the engine is only as good as the policy data — and for an admin runbook covering stuck queues, missed SLAs, cloud-sync reconciliation, and (new) widget origin management. Support-staff training is a larger cost than developer integration here, and if the widget is exposed, add a documented process for adding/removing embedding sites, since that list is now an access-control artifact rather than a cosmetic setting.
Overall TPM position. High capability per unit of integration effort, offset by concentrated stack coupling, non-trivial schema ownership, and thin adoption evidence. v1.9.4 strengthens the product's differentiation — self-serve embeddable support is a real differentiator — while adding a second public ingress and a config-as-security-boundary surface. Because the feature is default-off, the recommendation is unchanged in shape but needs one extra gate: recommend a scoped, reversible spike with explicit go/no-go criteria (clean schema review, verified authorization, documented uninstall), self-hosted mode only, internal-agent rollout before public portal, and widget embedding as a separately approved phase with an origin-allowlist review, CSRF-exemption scoping verified in code, and a metadata retention decision. Value the optionality: the low adoption that makes this cheap to try also makes it cheap to walk away from — protect that by not letting the package's migrations or frontend coupling become irreversible before the pilot proves out.
How can I help you explore Laravel packages today?