Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Service Desk Laravel Package

jeffersongoncalves/laravel-service-desk

Laravel Service Desk adds a ready-to-use helpdesk and ticketing layer to Laravel apps. Manage support tickets with categories, priorities, statuses, assigned agents, and customer conversations using included models, migrations, and workflow tools.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require jeffersonsgoncalves/laravel-service-desk
php artisan vendor:publish --tag="service-desk-config"
php artisan vendor:publish --tag="service-desk-migrations"
php artisan migrate

Then read, in this order:

  1. config/service-desk.php — it is the contract for everything else. Look for the model map (ticket, message, SLA policy, catalog item, article), the mail/inbound settings, and any queue/connection keys. As of v1.2.0 it also holds the ticket-sharing strategy and the new service-desk.connection key. Most "how do I change X" questions are answered here, not in code.
  2. Decide your sharing strategy before anything else — this is now a genuine fork in the road. Three options: database transport (satellite reads the tables directly), api transport (satellite calls the signed HTTP endpoints), or — new in v1.2.0 — the shared-database connection mode (UsesServiceDeskConnection trait + service-desk.connection), for apps that sit on one physical database but want the package's models pinned to an explicit connection. It's opt-in and a no-op unless configured. Pick one now; switching later means rewriting every satellite integration point.
  3. database/migrations/ — the schema is the real API. Read it before the README; it tells you the FK graph (ticket → requester → assignee → SLA policy → messages/attachments) and which columns are nullable, which matters a lot for reporting. v1.2.0 adds a uuid to ticket_comments, so comments now key on UUID like tickets and attachments.
  4. src/Contracts/ — headless packages expose interfaces for the pieces you're expected to swap (ticket number generation, SLA clock, email parser, notifier). These are your extension points.
  5. src/Events/ — the package emits ticket-created/updated/assigned/escalated/SLA-breached events. Subscribing to these is the supported integration path; subclassing internals is not.

First use case: open a ticket from your own controller and watch the SLA materialize.

use JeffersonGoncalves\ServiceDesk\Facades\ServiceDesk; // verify FQCN in src/

$ticket = ServiceDesk::openTicket([
    'subject'     => 'VPN drops every 10 minutes',
    'description' => '...',
    'requester'   => ['email' => $user->email, 'name' => $user->name],
    'priority'    => 'high',
    'channel'     => 'web',
    'department_id' => $departmentId, // discover valid IDs, see below
]);

dump($ticket->sla_due_at, $ticket->status, $ticket->reference);

If that works, the rest of the package is variations on it: replies, status transitions, catalog requests, and KB lookup.

Second use case — the satellite app flow (v1.2.0). If you're integrating from a second Laravel app, you no longer hardcode org structure. Resolve IDs first, then post:

$departments = Http::withHeaders($signed)->get("$base/tickets-api-prefix/departments");
$categories  = Http::withHeaders($signed)->get("$base/tickets-api-prefix/departments/{$id}/categories");

$transport->addComment($ticketUuid, ['body' => 'Customer confirmed the fix.']);
// == POST tickets/{uuid}/comments

A realistic caveat before you commit: 9 stars, 0 dependents, a single maintainer, and a release stamped 2026-09-21. Treat this as a codebase you are adopting rather than a dependency you are consuming — read the source of anything you rely on, and pin the exact version in composer.json.

Implementation Patterns

Build thin HTTP on top, keep logic in the package. Headless means you own routes, controllers, auth, and UI, and the package owns state transitions and SLA math. Resist the temptation to update status directly via $ticket->update([...]); route through whatever transition/service method the package provides (grep for transition, close, reopen, assign in src/) so SLA timers, audit rows, and events fire consistently. Direct column writes are how you get tickets with a closed_at and a still-running SLA clock.

SLA assignment is a creation-time concern. Typical shape:

// config/service-desk.php
'sla' => [
    'policies' => [
        // priority => [response_minutes, resolution_minutes]
        'urgent' => ['response' => 15,  'resolve' => 240],
        'high'   => ['response' => 60,  'resolve' => 480],
    ],
    'business_hours' => ['mon-fri 09:00-17:00'], // inspect actual key names
    'timezone'       => 'Europe/Lisbon',
],

// or resolve dynamically per tenant/customer
'ticket_model' => App\Models\Ticket::class, // extend the package model

If the package supports a resolver callable for SLA policy selection, use it — per-customer contract tiers are the #1 real-world SLA requirement and retrofitting them later means backfilling sla_due_at on live tickets.

Pick a sharing strategy and stay on it. database transport is simplest but couples your satellite to the package's schema forever. The api transport is the right default for a satellite you don't fully control — the new signed endpoints mean the satellite no longer needs the package installed, only an HTTP client, and v1.2.0 closes the two biggest API-mode gaps (creating comments, discovering reference data). Shared-connection mode is for the case where both apps already live on one database and you want the models explicitly bound to it rather than inheriting the default connection — use it when your main app's DB_CONNECTION is not where the service desk tables live.

Connection mode in practice. Set the key, and apply the trait to any model you substitute via config:

// config/service-desk.php
'connection' => env('SERVICE_DESK_DB', 'service_desk'),
final class Ticket extends \JeffersonGoncalves\ServiceDesk\Models\Ticket
{
    use \JeffersonGoncalves\ServiceDesk\Concerns\UsesServiceDeskConnection;
}

The release applies it to the six core models — Ticket, TicketComment, TicketAttachment, TicketHistory, Department, Category. Anything outside that set (SLA policies, catalog items, articles) may still resolve to the default connection; verify before assuming the whole package is fenced onto service_desk.

Department/category discovery replaces hardcoded IDs. Cache the two discovery endpoints on the satellite (they're static org data — a sensible TTL is hours, not seconds), and re-validate the department_id/category_id you send at intake. The endpoints are unscoped by design, so they do not belong in your authorization logic — only in your form dropdowns.

Comment surface is public-only, and that's the point. addComment() and POST tickets/{uuid}/comments cannot reach internal notes "by construction". Design around it rather than fighting it: internal notes stay in the agent UI, customer-visible replies come from satellites. If a satellite genuinely needs to write an internal note, that is not a supported surface — use the database transport or a queue job on the main app instead.

Drive notifications from events, not from the package's mailer. The package does email (inbound channel, and likely outbound notifications), but your product probably wants Slack, in-app, digest emails, and per-user preferences. Listen to TicketCreated, TicketReplied, TicketAssigned, SlaBreached and fan out yourself. This also keeps the door open to replacing the package later.

Event::listen(TicketReplied::class, function ($event) {
    NotifyAgentsJob::dispatch($event->ticket, $event->message)
        ->onQueue('notifications');
});

Inbound email needs a threading map and idempotency. If you wire up the mail channel, store message_id / in_reply_to / references and match on them before falling back to a [REF-1234] subject token. Deduplicate on message_id with a unique index — providers retry webhooks, and IMAP pollers re-read the same message after a crash. Push parsing to a queued job so a malformed MIME body can't 500 your webhook endpoint.

Key on UUIDs across the boundary. Tickets, attachments, and (since v1.2.0) comments all expose a uuid. Use it as the identifier in every cross-app call, every queued job payload, and every satellite-side mapping table. Auto-increment IDs are an implementation detail of whichever connection you happen to be on, and that assumption breaks the moment you switch sharing strategies.

Service catalog + approvals is a separate state machine from tickets. Model it explicitly: requested → pending_approval → approved|rejected → fulfilled. Fulfilment usually creates a ticket — keep that link (catalog_request_id on the ticket) so you can report on catalog demand versus incident volume. Approvals need an approver identity that is not the requester, and a fallback when the approver is the requester (self-approval lockout is a classic bug here).

Knowledge base as deflection, not just content. Before creating a ticket, query articles by catalog item / keywords and show suggestions on the intake form. The measurable win of this package is defect deflection rate; instrument it by tagging tickets created after a KB view.

Multi-tenant scoping. If there's a team_id/tenant_id on tickets, add a global scope on your extended model and add it in one place. Retrofitting tenancy after tickets exist means a painful backfill plus an audit of every query in your controllers.

Gotchas and Tips

  • Headless ≠ authorized. The package will not give you policies, gates, or scoping. Add TicketPolicy and CatalogRequestPolicy on day one, and always constrain index queries by the authenticated agent's team. An unguarded Ticket::find($id) in a controller is a data breach in a service desk. The new department/category endpoints are explicitly unscoped — treat their output as public reference data, never as a permission check.
  • Connection-mode is a two-part configuration. Setting service-desk.connection without applying UsesServiceDeskConnection to your substituted models (or vice versa) gives you a split brain: some models on the service desk connection, some on default. Symptom is "no such table" from one query and not the next. Verify with (new Ticket)->getConnectionName() in tinker before debugging anything else.
  • Only six models joined the connection party. Ticket, TicketComment, TicketAttachment, TicketHistory, Department, Category. SLA policies, catalog items, and articles are not in the release notes. If you're running a split database, check every model you touch resolves where you expect — and check relations: eager loading a ticket with slaPolicy can query a different connection than the parent.
  • Cross-connection writes aren't transactional. Wrapping a ticket write (connection A) and a domain write (connection B) in one DB::transaction() is a lie. Either keep them on one connection, or accept eventual consistency and drive the second write from an event/queued job with retry.
  • Who owns the migrations? In shared-connection mode, publish and run the package migrations from exactly one app. Two apps both running migrate against the same physical database will race, and migrations table rows diverge between the two installs. Pick the owner, document it, and have the other app skip the publish step.
  • Existing installs need a comment-UUID backfill check. v1.2.0 adds a uuid column to ticket_comments. Existing rows need a value — confirm the migration backfills (or generate one yourself) before the API resource starts keying on a null. This is precisely the class of change that only bites upgraded installs, not fresh ones.
  • The comment endpoint has no idempotency key in the notes. Retries on a 5xx or a timeout will double-post. Deduplicate on your side — a client-side correlation ID in the body, or a "last comment by this satellite within N seconds" guard.
  • Signed-request middleware fails on clock skew. The discovery endpoints sit behind it too, so a satellite with drifting time will get 401s on every department lookup and you'll blame the cache. Run NTP, keep the signing window generous-but-bounded, and log the signature mismatch rather than letting a 401 present as "no departments exist".
  • Timezones and business hours are where SLA math breaks. Store timestamps in UTC, compute due dates in the SLA's configured timezone, and test across a DST boundary and a weekend. Freeze time in tests ($this->travelTo(...)) rather than sleeping. If the package only does wall-clock minutes, you'll need to wrap its calculator with a business-hours implementation via the contract.
  • Pause semantics for "pending customer". Most SLAs stop the resolution clock while waiting on the requester. Check whether the package models this; if not, you'll be adding paused_at/resumed_at and adjusting due dates yourself. Do this before go-live — historical SLA reports become uninterpretable otherwise.
  • Model swapping is all-or-nothing. If you extend Ticket in App\Models, update the config map and check for hardcoded Ticket:: references or instanceof checks in the package. Migrations keyed to the package table name still work, but relations pointing at the old class won't. Grep src/ for the class name once when you swap — and since v1.2.0, grep for UsesServiceDeskConnection too, or your subclass silently drops back to the default connection.
  • Publishing migrations into a live app. Publish, then immediately diff against database/migrations/ in the package repo before running on production. Packages in this maturity range sometimes amend migrations between patch releases; the comment-UUID addition is a live example of schema moving under you. You'll only find out when a fresh install and your upgraded install diverge.
  • Email loops. Never send auto-replies to no-reply@, mailer-daemon@, or any address with Auto-Submitted: / Precedence: bulk headers. One badly configured out-of-office and the package happily generates a ticket per auto-reply, forever. The new comment route makes this worse if a satellite echoes every notification back in — never let an outbound notification trigger an inbound comment.
  • Debugging state transitions. Start with php artisan tinker on the ticket and inspect status, SLA columns, and timestamps; then check whether the expected event actually fired (Event::fake() in a quick test, or Log::listen). "The SLA didn't set" is nearly always "policy didn't match because priority was null" — validate priority at intake rather than letting it default silently.
  • Queue everything external. Inbound mail parsing, outbound notifications, SLA sweep/breach checks, satellite sync, and attachment processing. The breach detector in particular should be a scheduled command with an overlap lock (withoutOverlapping()), or you'll email customers twice when a slow run overlaps.
  • Test coverage you must own. Factories for tickets/messages, tests for: SLA due date on creation, breach event firing, reply threading, approval rejection path, tenant isolation, and (new) the connection binding itself — RefreshDatabase against the configured connection, plus a test that internal notes are not reachable via addComment().
  • Upgrade posture. Pin "jeffersonsgoncalves/laravel-service-desk": "1.x" with a lockfile committed, and keep a short changelog of every package method you call. The addition of a third transport plus a cross-cutting connection trait in a single minor release is the signal that this surface will keep moving — you are effectively the maintainer of your integration layer, and the smaller that surface, the cheaper the day you have to vendor the code or replace it.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table