No description
  • PHP 73.5%
  • Blade 20.8%
  • CSS 4.2%
  • JavaScript 1.5%
Find a file
2026-06-25 15:07:34 +00:00
config Initial import: lithiumhosting/laravel-helpdesk v0.1.0 2026-06-23 23:06:40 +00:00
database/migrations Initial import: lithiumhosting/laravel-helpdesk v0.1.0 2026-06-23 23:06:40 +00:00
dist Initial import: lithiumhosting/laravel-helpdesk v0.1.0 2026-06-23 23:06:40 +00:00
resources Initial import: lithiumhosting/laravel-helpdesk v0.1.0 2026-06-23 23:06:40 +00:00
routes Initial import: lithiumhosting/laravel-helpdesk v0.1.0 2026-06-23 23:06:40 +00:00
src Initial import: lithiumhosting/laravel-helpdesk v0.1.0 2026-06-23 23:06:40 +00:00
tests Initial import: lithiumhosting/laravel-helpdesk v0.1.0 2026-06-23 23:06:40 +00:00
.gitignore Initial import: lithiumhosting/laravel-helpdesk v0.1.0 2026-06-23 23:06:40 +00:00
CHANGELOG.md Point canonical URLs at git.lithium.hosting 2026-06-23 23:11:28 +00:00
composer.json Point canonical URLs at git.lithium.hosting 2026-06-23 23:11:28 +00:00
LICENSE Initial import: lithiumhosting/laravel-helpdesk v0.1.0 2026-06-23 23:06:40 +00:00
package.json Initial import: lithiumhosting/laravel-helpdesk v0.1.0 2026-06-23 23:06:40 +00:00
phpunit.xml Initial import: lithiumhosting/laravel-helpdesk v0.1.0 2026-06-23 23:06:40 +00:00
README.md docs: add 'Is it any good?' section to match the package README convention 2026-06-25 15:07:34 +00:00
tailwind.config.js Initial import: lithiumhosting/laravel-helpdesk v0.1.0 2026-06-23 23:06:40 +00:00
UPGRADING.md Initial import: lithiumhosting/laravel-helpdesk v0.1.0 2026-06-23 23:06:40 +00:00

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_staff column, 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 mail and database. The database channel needs Laravel's notifications table. If you don't already have it:

php artisan make:notifications-table   # Laravel 11+
php artisan migrate

Prefer email only? Set helpdesk.notifications.channels to ['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:

  1. A closure you register (full control).
  2. A configured Gate ability.
  3. 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-prose plain-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 point helpdesk.layout at 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 as helpdesk::... 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.".