- PHP 73.5%
- Blade 20.8%
- CSS 4.2%
- JavaScript 1.5%
| config | ||
| database/migrations | ||
| dist | ||
| resources | ||
| routes | ||
| src | ||
| tests | ||
| .gitignore | ||
| CHANGELOG.md | ||
| composer.json | ||
| LICENSE | ||
| package.json | ||
| phpunit.xml | ||
| README.md | ||
| tailwind.config.js | ||
| UPGRADING.md | ||
Laravel HelpDesk
A self-contained helpdesk for Laravel apps: a knowledgebase and support
tickets with staff admin tooling, canned replies, and notifications. Drop it
into an existing app, point it at your User model, and you have a customer
support surface in minutes. Use both features or just one.
- Knowledgebase: categorized, Markdown articles with search. Server-rendered Blade.
- Support tickets: customer ticket form + threaded replies, staff queue, assignment, statuses, internal notes. Livewire-powered.
- Optional taxonomies: departments, categories, priorities. Turn any of them on or off.
- Canned replies: saved staff responses with
{{ first_name }}/{{ last_name }}/{{ full_name }}substitution. - Notifications: mail + database (for in-app bells), plus domain events you can subscribe to.
- Configurable staff recognition: works with an
is_staffcolumn, a method, a Gate, or your own closure.
Requirements
- PHP 8.2+
- Laravel 11 or 12
- Livewire 3 (ticket UI + admin are Livewire components)
Compatibility
| Dependency | Supported |
|---|---|
| PHP | ^8.2 |
Laravel (illuminate/*) |
11 & 12 (^11.0|^12.0) |
| Livewire | ^3.0 |
The asset build (maintainers only) uses Tailwind CSS v3.4 with
@tailwindcss/forms. That toolchain is Node, dev-only, and never required on the
consumer side: the package ships the compiled dist/helpdesk.css. Consumers
publish the stylesheet, they do not build it.
Installation
From the Git repository (Composer VCS)
There is no Satis or Packagist. Add the package's git repository directly as a
Composer vcs repository, then require it. Composer reads composer.json from
the repository's tags, so versions resolve from the v* tags (e.g. v0.1.0).
// your app's composer.json
"repositories": [
{
"type": "vcs",
"url": "https://git.lithium.hosting/LithiumHosting/laravel-helpdesk.git"
}
]
composer require lithiumhosting/laravel-helpdesk:^0.1
The repository is public, so no Composer auth is needed.
Local path repository (working on the package itself)
// composer.json
"repositories": [
{
"type": "path",
"url": "local-packages/lithiumhosting/laravel-helpdesk",
"options": { "symlink": true }
}
],
"require": {
"lithiumhosting/laravel-helpdesk": "*"
}
composer update lithiumhosting/laravel-helpdesk
Publish + migrate
php artisan vendor:publish --tag=helpdesk-config # config/helpdesk.php
php artisan vendor:publish --tag=helpdesk-assets # public/vendor/helpdesk/helpdesk.css
php artisan migrate # package tables auto-load; no publish needed
The package's migrations load automatically. Publish them only if you want to
edit them: --tag=helpdesk-migrations.
The package ships a pre-compiled Tailwind stylesheet (dist/helpdesk.css).
Publishing it (--tag=helpdesk-assets) copies it to
public/vendor/helpdesk/helpdesk.css, and the default layout links it
automatically. If you skip this step the layout falls back to the Tailwind CDN,
so the pages still render, but publish for production (the CDN build is not
meant for prod). Re-run the publish (with --force) after upgrading the package
to pick up stylesheet changes. You do not need to run the package's own
npm run build to use it. That is only for maintainers (see below).
Database notifications (in-app bells). The default notification channels are
database. Thedatabasechannel needs Laravel'snotificationstable. If you don't already have it:php artisan make:notifications-table # Laravel 11+ php artisan migratePrefer email only? Set
helpdesk.notifications.channelsto['mail'].
Configuration
Everything lives in config/helpdesk.php. Highlights:
'features' => [
'tickets' => true, // customer ticket form + staff queue
'knowledgebase' => true, // KB articles + search
],
'tickets' => [
'departments' => true, // optional taxonomy pickers
'categories' => true,
'priorities' => true,
'allow_reopen' => true, // can a closed ticket be reopened?
],
'notifications' => [
'channels' => ['mail', 'database'],
'support_email' => env('HELPDESK_SUPPORT_EMAIL'),
],
'layout' => 'helpdesk::layouts.app', // point at your own layout (see below)
'routes' => ['prefix' => 'support', 'admin_prefix' => 'support/admin', 'middleware' => ['web', 'auth']],
'tables' => ['prefix' => 'helpdesk_'],
'teams' => ['enabled' => false], // reserved for a future team-routing feature
Feature toggles
- Tickets only: set
features.knowledgebase => false. KB routes + nav disappear. - KB only: set
features.tickets => false. The whole ticket surface, staff queue, and ticket admin disappear. - Bare tickets: leave tickets on but turn off
tickets.departments/categories/priorities. The form is just subject + message.
Staff recognition (required setup)
The package needs to know which users are staff. Resolution order:
- A closure you register (full control).
- A configured Gate ability.
- Truthiness of any configured attribute/accessor/method on the user.
For an app with an is_staff or is_admin column, the defaults already
work, nothing to do. Otherwise register a closure in your
AppServiceProvider::boot():
use LithiumHosting\HelpDesk\Facades\HelpDesk;
HelpDesk::staffUsing(fn ($user) => $user->is_staff || $user->hasRole('support'));
Or via config:
// config/helpdesk.php
'staff' => [
'attributes' => ['is_staff', 'is_admin'], // column, accessor, or no-arg method
'gate' => 'access-helpdesk', // optional Gate ability
],
Notifying staff of new tickets
The staff resolver answers "is this user staff?" It cannot enumerate the team. To notify staff on new tickets, register who they are:
HelpDesk::staffNotifiablesUsing(fn () => User::where('is_staff', true)->get());
If you don't register this, new-ticket alerts fall back to
helpdesk.notifications.support_email (email only). Replies to assigned
tickets always notify the assignee directly.
Usage
Routes
With the default support prefix:
| Route name | Path | Who |
|---|---|---|
helpdesk.kb.index |
/support/kb |
anyone (per middleware) |
helpdesk.kb.article |
/support/kb/{slug} |
anyone |
helpdesk.tickets.index |
/support/tickets |
the customer (their tickets) |
helpdesk.tickets.create |
/support/tickets/create |
the customer |
helpdesk.tickets.show |
/support/tickets/{reference} |
owner or staff |
helpdesk.staff.tickets.index |
/support/staff/tickets |
staff only |
helpdesk.admin.dashboard |
/support/admin |
staff only |
Link from your app's nav:
<a href="{{ route('helpdesk.tickets.index') }}">Support</a>
@if (\LithiumHosting\HelpDesk\Facades\HelpDesk::isStaff(auth()->user()))
<a href="{{ route('helpdesk.admin.dashboard') }}">Helpdesk admin</a>
@endif
Admin
/support/admin (staff-gated) manages KB articles + categories, ticket
departments / categories / priorities, and canned replies, all inline CRUD.
Canned replies
Staff pick a saved reply from the ticket thread; {{ first_name }},
{{ last_name }}, and {{ full_name }} are substituted from the ticket's
customer. The renderer is tolerant of your user's name shape: it uses explicit
first_name / last_name / full_name attributes when present, otherwise
splits a single name.
Events (hook your own notification bell)
Every notification-worthy action also fires a domain event:
use LithiumHosting\HelpDesk\Events\{TicketCreated, TicketReplied, TicketClosed};
Event::listen(TicketReplied::class, function (TicketReplied $e) {
// $e->ticket, $e->reply (e.g. push to your own bell / Slack)
});
The package's own mail + database notifications are independent of these events, so you can use either or both.
Making it first-party (full walkthrough)
To make the helpdesk feel like a native part of your app (your chrome, your palette, your dark mode), wire these steps. The two seams (layout + tokens) are detailed under Theming below; this is the end-to-end recipe.
1. Install, publish, migrate
# Add the vcs repository to composer.json first (see Installation), then:
composer require lithiumhosting/laravel-helpdesk:^0.1
php artisan vendor:publish --tag=helpdesk-config
php artisan vendor:publish --tag=helpdesk-assets
php artisan migrate
2. Point it at your app layout (config/helpdesk.php):
'layout' => 'layouts.app',
Your layout needs three things. First, load the package stylesheet in <head>
before your own CSS (so your token overrides win), only on helpdesk routes:
@if (request()->routeIs('helpdesk.*') && is_file(public_path('vendor/helpdesk/helpdesk.css')))
<link rel="stylesheet" href="{{ asset('vendor/helpdesk/helpdesk.css') }}?v={{ filemtime(public_path('vendor/helpdesk/helpdesk.css')) }}">
@endif
@vite(['resources/css/app.css', 'resources/js/app.js']) {{-- your app CSS, after --}}
Second, surface the package's flash keys (it flashes to helpdesk.success /
helpdesk.error):
@if (session('helpdesk.success')) <div class="...">{{ session('helpdesk.success') }}</div> @endif
@if (session('helpdesk.error')) <div class="...">{{ session('helpdesk.error') }}</div> @endif
Third, render the content. The package injects Livewire pages via {{ $slot }}
and KB pages via @yield('helpdesk'). Contain the width however you like (the
package does not constrain its own):
<main>
@if (request()->routeIs('helpdesk.*'))
<div class="mx-auto w-full max-w-7xl px-4 sm:px-6 lg:px-8 py-8">
{{ $slot ?? '' }}
@yield('helpdesk')
</div>
@else
{{ $slot ?? '' }}
@yield('helpdesk')
@endif
</main>
3. Alias the color tokens in your app CSS :root (see Theming
for the full list). If your tokens flip under .dark, dark mode is inherited:
:root {
--hd-primary: var(--brand-primary);
--hd-surface: var(--surface-card);
--hd-text: var(--text-primary);
--hd-border: var(--border-subtle);
/* ...the rest... */
}
4. Tell it who staff are (in a service provider boot()):
use LithiumHosting\HelpDesk\Facades\HelpDesk;
// "Is this user staff?" The default already reads is_staff / is_admin
// columns, so this is only needed for custom logic:
HelpDesk::staffUsing(fn ($user) => $user->is_admin);
// Who receives new-ticket mail (the package can't enumerate your team):
HelpDesk::staffNotifiablesUsing(fn () => User::where('is_admin', true)->get());
5. Choose notification channels (config/helpdesk.php). Use mail only
unless you have Laravel's notifications table:
'notifications' => ['channels' => ['mail']],
6. Link to it from your nav using the route names: helpdesk.kb.index,
helpdesk.tickets.index, helpdesk.tickets.create,
helpdesk.staff.tickets.index (staff), helpdesk.admin.dashboard (staff).
That is the whole integration: helpdesk renders in your chrome and palette, dark mode is inherited, notifications go out by mail, and staff are your admins.
Theming
The package is built to match any app's design system through two seams: layout (you supply the chrome) and color tokens (you supply the palette). No view forking required.
1. Layout (chrome)
Package pages render through config('helpdesk.layout'), which defaults to a
self-contained layout that links the published stylesheet (or the Tailwind CDN
as a fallback) so it renders out of the box. Point it at your own layout to wrap
the helpdesk in your app chrome:
'layout' => 'layouts.app',
Your layout renders {{ $slot }} (Livewire ticket/admin pages) and
@yield('helpdesk') (KB pages). Width is your call: the package views don't
constrain their own width, so wrap them in your layout if you don't want them
full-bleed, e.g. <div class="mx-auto max-w-5xl px-4">…</div>.
2. Color tokens
Every color resolves through a CSS variable (RGB channel triplet, so Tailwind opacity utilities keep working). Override any of these to retheme; the package ships defaults for all of them. Alias them to your own design tokens and dark mode comes through automatically:
/* your app, loaded after the package stylesheet */
:root {
--hd-primary: var(--brand-primary); /* or: 37 99 235 */
--hd-primary-hover: var(--brand-primary-hover);
--hd-primary-soft: var(--brand-primary-soft);
--hd-page: var(--surface-page);
--hd-surface: var(--surface-card);
--hd-surface-muted: var(--surface-sunken);
--hd-elevated: var(--surface-elevated);
--hd-text: var(--text-primary);
--hd-text-muted: var(--text-muted);
--hd-text-subtle: var(--text-disabled);
--hd-border: var(--border-subtle);
--hd-border-strong: var(--border-strong);
--hd-success: var(--success); --hd-success-soft: var(--success-soft);
--hd-warning: var(--warning); --hd-warning-soft: var(--warning-soft);
--hd-danger: var(--danger); --hd-danger-soft: var(--danger-soft);
}
Because these alias via var(), if your tokens already flip under a .dark
class (Tailwind darkMode: 'class'), the helpdesk inherits dark mode with no
extra block. Load order matters: load the package CSS before your app CSS so
your :root overrides win over the package defaults.
Need structural changes (not just color)? Publish and edit the views as a last resort:
php artisan vendor:publish --tag=helpdesk-views # resources/views/vendor/helpdesk
Building the stylesheet (maintainers)
Consumers just publish dist/helpdesk.css; they never build it. Maintainers
regenerate it after changing any package view, since Tailwind only emits the
utility classes it finds:
npm install # in the package root
npm run build # writes dist/helpdesk.css (scans resources/views + src)
dist/helpdesk.css is committed so the package works without a Node toolchain
on the consumer side. The Tailwind config (tailwind.config.js) scans
resources/views/**/*.blade.php and src/**/*.php, so any class used in a
Blade view or emitted from PHP is included.
Embedding (preflight-free)
The compiled stylesheet is built without Tailwind's preflight reset so it is safe to embed alongside a host's own Tailwind build: it will not fight the host's base reset or restyle elements outside the helpdesk. The trade-off is that the package does not bring its own base reset.
- Embedded (loaded next to a host Tailwind build, the first-party setup): it
relies on the host's base reset. Knowledgebase Markdown bodies are unaffected
because their typography comes from the
.hd-proseplain-CSS rules (themed through--hd-*), not from preflight. - Standalone (the package's default
helpdesk::layouts.app): the layout pulls in the Tailwind CDN as a fallback, which supplies a base reset, so the pages render correctly with no host CSS. If you pointhelpdesk.layoutat your own bare layout without a host Tailwind build, supply a base reset yourself.
Database tables
All prefixed with helpdesk_ (configurable): kb_categories, kb_articles,
ticket_departments, ticket_categories, ticket_priorities, tickets,
ticket_replies, canned_replies. User references are stored as
unsignedBigInteger without a hard foreign key, so the package doesn't assume
your users table name or key type.
Model map / extending models
Every package model resolves its class through config('helpdesk.models.*'), so
you can add relationships, casts, scopes, or accessors without forking the
package. Point a key at your own subclass; the subclass must extend the
package model it replaces. Relationships, queries, and route resolution inside
the package all honor the binding.
// app/Models/Ticket.php
namespace App\Models;
use LithiumHosting\HelpDesk\Models\Ticket as HelpDeskTicket;
class Ticket extends HelpDeskTicket
{
public function project()
{
return $this->belongsTo(Project::class);
}
}
// config/helpdesk.php
'models' => [
'ticket' => \App\Models\Ticket::class,
// ...leave the rest pointing at the package models...
],
The mappable keys are: ticket, ticket_reply, ticket_department,
ticket_category, ticket_priority, canned_reply, kb_article,
kb_category.
Resolve a bound model in your own code through the facade:
use LithiumHosting\HelpDesk\Facades\HelpDesk;
HelpDesk::model('ticket'); // class-string of the bound Ticket model
HelpDesk::query('ticket'); // fresh Eloquent builder for the bound model
Internationalization
Every user-facing string lives in publishable lang files under the helpdesk::
translation namespace. To customize wording (or translate), publish them:
php artisan vendor:publish --tag=helpdesk-lang # lang/vendor/helpdesk/en/*.php
Then reword the strings in lang/vendor/helpdesk/en/*.php. The files are
admin.php, kb.php, nav.php, notifications.php, and tickets.php. Add a
sibling locale directory (for example lang/vendor/helpdesk/es/) to translate
into another language.
Public API surface (semver)
The following surfaces are covered by Semantic Versioning. A breaking change to any of them bumps the appropriate semver segment. See UPGRADING.md for the upgrade checklist and CHANGELOG.md for what changed in each release. Anything not listed here is internal and may change without a major bump.
- Config keys under
helpdesk.*(config/helpdesk.php). - Route names:
helpdesk.kb.index,helpdesk.kb.search,helpdesk.kb.category,helpdesk.kb.article,helpdesk.tickets.index,helpdesk.tickets.create,helpdesk.tickets.show,helpdesk.staff.tickets.index,helpdesk.admin.dashboard,helpdesk.admin.kb-categories,helpdesk.admin.kb-articles,helpdesk.admin.departments,helpdesk.admin.categories,helpdesk.admin.priorities,helpdesk.admin.canned-replies. - The
helpdesk::view namespace (referenced ashelpdesk::...in includes /loadViewsFrom). - The
helpdesk::lang namespace and its files:admin.php,kb.php,nav.php,notifications.php,tickets.php. - Facade methods on
LithiumHosting\HelpDesk\Facades\HelpDesk:staffUsing,staffNotifiablesUsing,isStaff,staffNotifiables,hasStaffNotifiablesResolver,featureEnabled,ticketsEnabled,knowledgebaseEnabled,userModel,model,query,table. - Notification events:
LithiumHosting\HelpDesk\Events\TicketCreated,TicketReplied,TicketClosed(class names + their readonly constructor properties). --hd-*CSS variable names:--hd-primary,--hd-primary-hover,--hd-primary-soft,--hd-page,--hd-surface,--hd-surface-muted,--hd-elevated,--hd-text,--hd-text-muted,--hd-text-subtle,--hd-border,--hd-border-strong,--hd-success,--hd-success-soft,--hd-warning,--hd-warning-soft,--hd-danger,--hd-danger-soft.- Publish tags:
helpdesk-config,helpdesk-migrations,helpdesk-views,helpdesk-assets,helpdesk-lang. - Published file paths:
config/helpdesk.php,resources/views/vendor/helpdesk/**,lang/vendor/helpdesk/**,public/vendor/helpdesk/helpdesk.css, and the package migrations.
License
The MIT License (MIT). Please see the License File for more information.
Is it any good?
Yes.
When people first hear about a new product, they frequently ask if it is any good. A Hacker News user remarked:
Note to self: Starting immediately, all raganwald projects will have a "Is it any good?" section in the readme, and the answer shall be "yes.".