a2zwebltd/laravel-customer-support
| Install | |
|---|---|
composer require a2zwebltd/laravel-customer-support |
|
| Latest Version: | v1.1.0 |
| PHP: | ^8.2 |
| License: | MIT |
| Last Updated: | Sep 23, 2026 |
| Links: | GitHub · Packagist |
Laravel Customer Support
A portable Laravel customer-support / helpdesk engine — tickets with threaded replies, attachments, internal notes, agent assignment, SLA tracking, mail notifications, a Livewire + Flux UI, and Nova admin resources.
Designed to drop into any Laravel app with minimal wiring while remaining fully customisable.
Screenshots
| Agent dashboard | New ticket |
|---|---|
![]() |
![]() |
| Ticket details | Nova admin |
|---|---|
![]() |
![]() |
Requirements
- PHP 8.2+
- Laravel 11 / 12 / 13
livewire/livewire^3 or ^4 (for the bundled UI)livewire/flux^2 (recommended — Blade templates use Flux components)spatie/laravel-medialibrary^11 (attachments)laravel/nova^5 (optional — auto-registers Nova resources when present)
Installation
composer require a2zwebltd/laravel-customer-support
php artisan migrate
php artisan vendor:publish --tag=customer-support-config # optional
Add the trait to your User model:
use A2ZWeb\CustomerSupport\Concerns\HasSupportTickets;
class User extends Authenticatable
{
use HasSupportTickets;
}
Define the agent gate in AppServiceProvider::boot():
Gate::define('manage-support-tickets', fn (User $user) => $user->is_admin);
Without this gate nobody counts as an agent: agent replies are treated as customer replies (wrong status, no first_response_at) and /support/admin returns 403 for everyone.
Features
- Ticket statuses: Open, Pending, In Progress, Awaiting Customer, Resolved, Closed
- Priorities: Low / Normal / High / Urgent — each with configurable SLA hours
- Categories with configurable labels and icons (keys come from the
TicketCategoryenum) - Threaded replies (
SupportTicketMessage) - Internal notes (visible only to agents)
- Attachments via
spatie/laravel-medialibrary(on tickets and messages) - Agent assignment with
assigned_to - SLA timer (
due_at) +EscalateOverdueTicketsconsole command for cron escalation - Mail notifications: created / replied / status-changed / resolved (markdown, queueable)
- Domain events:
TicketCreated,TicketReplied,TicketStatusChanged,TicketAssigned - Policies on ticket + message resources
- Livewire + Flux UI that picks up your Flux accent colour (dark-mode aware)
- Every user-facing string translatable through your
lang/*.json; customer mails go out in the customer's locale - Attachments served through an authenticated, policy-checked route, so a private disk works out of the box
- Nova resources auto-registered when Nova is installed
Routes
| Method | URI | Name |
|---|---|---|
| GET | /support |
support.index |
| GET | /support/new |
support.create |
| GET | /support/{ticket} |
support.show |
| GET | /support/{ticket}/attachments/{media uuid} |
support.attachment |
| GET | /support/admin |
support.admin.dashboard |
(Prefix, name prefix and middleware configurable in config/customer-support.php.) Views and mails build every link from routes.name_prefix, so a custom prefix such as app/support is carried into the mail buttons too. If you set routes.enabled to false, declare all five routes yourself under the same names, including attachment.
Configuration
See config/customer-support.php. Highlights: user_model, routes, admin_gate, categories, sla_hours, mail.admin_recipients, attachments, nova, layout.
categorieskeys must beTicketCategoryenum values (bug,question,billing,feature_request,account,other). You can relabel, re-icon, reorder or remove them. The form saves unknown keys asother.nova.register_resources(defaulttrue),nova.group(defaultFeedback) andnova.user_resource(the Nova resource used for customer / assignee / author fields, defaultApp\Nova\User).layout(envCUSTOMER_SUPPORT_LAYOUT, defaultcomponents.layouts.app) must name a layout view that exists in your app.
Attachments and private disks
Attachments are stored with spatie/laravel-medialibrary on attachments.disk. The UI never links a public file URL. It links support.attachment, which:
- checks that the file belongs to that ticket or one of its messages (anything else is a 404),
- authorises
viewthrough the message policy, so internal-note files are agent-only and other customers get a 403, - streams the file from its disk (local, private S3 or public) with
Cache-Control: private, no-storeandX-Content-Type-Options: nosniff. Images, PDFs and plain text open inline, everything else downloads. Add?download=1to force a download.
disk stays public by default for backwards compatibility. Anyone who learns a public-disk URL can still fetch the file, so for confidential data use a private disk:
'attachments' => [
'disk' => 'local', // or a private S3 disk
// ...
],
The setting applies to new uploads. Existing files stay on their old disk (the media.disk column) and are still served through the route.
Translations
Every user-facing string in the views, mails, mail subjects, enum labels (status, priority, category) and SLA labels goes through __() with the English sentence as the key and :placeholders for values, e.g. __('We received your ticket :number', ['number' => ...]). The package ships no lang files. Translate the keys you need in your app's lang/pl.json, lang/de.json and so on:
{
"We received your ticket :number": "Otrzymaliśmy Twoje zgłoszenie :number",
"Open a support ticket": "Otwórz zgłoszenie",
"Overdue by :time": "Po terminie o :time"
}
Category labels set in categories are used as translation keys as well.
Mail locale
Customer mails are addressed to the user model (Mail::to($user)), not to a bare address. If your User implements Illuminate\Contracts\Translation\HasLocalePreference, Laravel renders the subject and body in preferredLocale():
class User extends Authenticatable implements HasLocalePreference
{
public function preferredLocale(): string
{
return $this->locale ?? config('app.locale');
}
}
Agent mails go to the addresses in mail.admin_recipients and use the app locale.
Styling (Tailwind v4)
Accent colours come from Flux's theme variables, through Tailwind arbitrary values such as bg-[var(--color-accent)], text-[var(--color-accent-content)], text-[var(--color-accent-foreground)] and color-mix() tints of --color-accent. The support pages therefore follow your brand: set the accent once in your theme and they pick it up, in light and dark mode.
@theme {
--color-accent: var(--color-indigo-600);
--color-accent-content: var(--color-indigo-700);
--color-accent-foreground: var(--color-white);
}
Without Flux, define those three variables yourself (plus their dark-mode values).
Tailwind v4 does not scan vendor/, so add the package views as a source in resources/css/app.css, then rebuild:
@source '../../vendor/a2zwebltd/laravel-customer-support/resources/views/**/*.blade.php';
To change anything else, publish the views with php artisan vendor:publish --tag=customer-support-views, edit them, and add @source '../views/vendor/customer-support/**/*.blade.php';. Published views take precedence over the package's and composer update does not refresh them, so re-sync them after upgrading.
Up to v1.1 the views used a fixed teal accent. Since v1.2.0 they use your Flux accent.
The
theme.accentconfig key was removed in v1.1.0. It never reached the views.
Cron
// routes/console.php
use Illuminate\Support\Facades\Schedule;
Schedule::command('support:escalate-overdue')->hourly();
Testing
composer test
AI agents (Laravel Boost)
The package ships Laravel Boost resources: a short guideline (resources/boost/guidelines/core.blade.php) and the customer-support-development skill (resources/boost/skills/). They need Boost 2 or newer in the host app:
composer require laravel/boost --dev
php artisan boost:install # first time
php artisan boost:update --discover # already using Boost
Select a2zwebltd/laravel-customer-support when Boost lists the packages.
Security Vulnerabilities
If you discover a security vulnerability, please report it responsibly through private communication with the maintainers.
License
The MIT License (MIT). Please see License File for more information.
Credits
Developed and maintained by the A2Z WEB crew:
- Dawid Makowski
- Website: https://a2zweb.co/
- GitHub: https://github.com/a2zwebltd/
Related Packages
Filament admin panel integration for the Escalated support ticket system
Laravel SDK for ChaosDesk: drop-in Livewire support forms, automatic diagnostic...
Filament plugin for jeffersongoncalves/laravel-service-desk - provides Admin, Ag...
Filament plugins for jeffersongoncalves/laravel-help-desk — User, Operator, and...



