Building a Support Ticket System

Customer issues arrive through email, live chat, social DMs, and the occasional phone call. Nobody knows who is handling what, the same complaint gets two different answers, and an...

Building a Support Ticket System

Customer issues arrive through email, live chat, social DMs, and the occasional phone call. Nobody knows who is handling what, the same complaint gets two different answers, and another one slips through until the customer is furious. That is rarely a people problem. It is the lack of one place to log and track every request.

A ticket system fixes that: every request becomes a record with a number, a status, an owner, and a deadline. This guide covers the features you actually need, the database design, Laravel 11 code for the core pieces, and when an off-the-shelf help desk is the smarter choice.

The Core Features

Do not start from an enterprise help desk feature list. For a team of one to ten people, these six cover most needs:

  1. A unique, speakable ticket number such as TKT-2610-0042, so customers and staff refer to the same thing.
  2. Clear statuses: open, in progress, waiting on customer, resolved, closed.
  3. Priorities: low, normal, high, urgent, each with a written definition.
  4. Assignment to one agent, not to "the team."
  5. A threaded conversation, including internal notes customers never see.
  6. SLA targets: first-response and resolution deadlines by priority.

Build or Buy?

CriteriaSaaS help desk (Zendesk, Freshdesk, Help Scout)Open source (osTicket, FreeScout)Custom Laravel build
Time to launchHoursA day or twoSeveral weeks
CostPer-agent monthly subscriptionHosting onlyDeveloper time + hosting
Access to your app's dataVia API or integrationsRequires modificationSame database, direct
Workflow customizationWithin settingsPossible, someone else's codeUnlimited
Best forDedicated support teamsTight budgetsTickets tied to your product and accounts

Building makes sense when tickets must live next to your application data, for example when a customer opens a ticket from an order page and the agent instantly sees payment and license status without switching tools.

Database Design

Two new tables plus your existing users table are enough for version one. If relational modeling is new to you, start with designing a simple ERD.

Schema::create('tickets', function (Blueprint $table) {
    $table->id();
    $table->string('number', 20)->unique();
    $table->foreignId('user_id')->constrained();
    $table->foreignId('assigned_to')->nullable()->constrained('users');
    $table->string('subject');
    $table->string('category', 50)->nullable();
    $table->enum('priority', ['low', 'normal', 'high', 'urgent'])->default('normal');
    $table->enum('status', ['open', 'in_progress', 'waiting_customer', 'resolved', 'closed'])->default('open');
    $table->timestamp('first_response_due_at')->nullable();
    $table->timestamp('resolution_due_at')->nullable();
    $table->timestamp('first_responded_at')->nullable();
    $table->timestamp('resolved_at')->nullable();
    $table->timestamps();
    $table->index(['status', 'priority']);
});

Schema::create('ticket_messages', function (Blueprint $table) {
    $table->id();
    $table->foreignId('ticket_id')->constrained()->cascadeOnDelete();
    $table->foreignId('user_id')->constrained();
    $table->text('body');
    $table->boolean('is_internal')->default(false);
    $table->timestamps();
});

Keep first_responded_at and resolved_at separate from updated_at. Without them you cannot measure SLA compliance, because updated_at changes on every small edit.

Ticket Numbers and SLA Deadlines

Compute the number and deadlines in one place, such as the model's creating event:

class Ticket extends Model
{
    protected $guarded = [];

    // hours: [first response, resolution]
    public const SLA_HOURS = [
        'urgent' => [1, 8],
        'high'   => [4, 24],
        'normal' => [8, 72],
        'low'    => [24, 120],
    ];

    protected static function booted(): void
    {
        static::creating(function (Ticket $ticket) {
            $ticket->number ??= 'TKT-' . now()->format('ym') . '-' . strtoupper(Str::random(5));

            [$respond, $resolve] = self::SLA_HOURS[$ticket->priority ?? 'normal'];
            $ticket->first_response_due_at = now()->addHours($respond);
            $ticket->resolution_due_at     = now()->addHours($resolve);
        });
    }
}

These SLA numbers are examples only. Set targets your team can actually hit; an SLA everyone routinely misses is worse than none, because breaking it becomes normal. If you support customers across time zones, consider whether deadlines count calendar hours or business hours, and document the choice.

When an agent sends a public reply, set first_responded_at if it is still empty and move the status to waiting_customer. Internal notes should never count as a response.

Notifications and SLA Reminders

  • To the customer: ticket received (with its number), new reply, ticket resolved.
  • To the agent: ticket assigned, customer replied.
  • To a supervisor: ticket approaching or past its SLA deadline.

Send mail through a queue so pages stay fast; see Laravel queues and jobs. For SLA warnings, schedule a check in routes/console.php:

Schedule::call(function () {
    Ticket::whereIn('status', ['open', 'in_progress'])
        ->whereNull('first_responded_at')
        ->where('first_response_due_at', '<=', now()->addMinutes(30))
        ->each(fn ($t) => Notification::route('mail', config('support.supervisor_email'))
            ->notify(new SlaWarning($t)));
})->everyFifteenMinutes();

In production, add a flag column such as sla_warned_at so the same warning is not resent every 15 minutes.

Common Mistakes

  1. Too many statuses. Ten options confuse agents; five is enough.
  2. Letting customers set priority. Nearly everyone picks "urgent." Let customers choose a category, and let agents or rules decide priority.
  3. Tickets without an owner. Assign every ticket to one named person.
  4. Missing authorization. Customer A must not open customer B's ticket by changing an ID in the URL. Use a Policy that checks user_id; see web application security best practices.
  5. Auto-closing too aggressively. Closing "waiting on customer" tickets after a few quiet days is fine, but warn the customer first.

A Knowledge Base to Reduce Volume

After a month or two, group tickets by category. Questions that keep repeating, such as password resets or installation steps, deserve help articles. Suggest matching articles on the ticket form based on the subject line, and some customers will solve the issue before they hit submit.

Metrics Worth Watching

Once tickets are logged properly, you gain data you never had before. Four numbers are enough for a weekly review:

  • First response time: the gap between created_at and first_responded_at. Report the median, not the average, so one forgotten ticket does not distort the picture.
  • SLA hit rate: tickets answered before first_response_due_at divided by all tickets.
  • Open tickets per agent: shows who is overloaded before quality drops.
  • Top categories: a signal of which part of your product confuses customers most.

For example, if "installation" tops the list every week, better docs or a short walkthrough video will likely cut volume more than hiring another agent.

Launch Checklist

  • Priority definitions and SLA targets are agreed and written down.
  • Every ticket has a unique number and a single owner.
  • Internal notes never reach customers (test it manually).
  • A Policy blocks access to other customers' tickets.
  • Mail goes through a queue and a worker is running.
  • The scheduler is active for SLA reminders.
  • Old channels send an auto-reply pointing to the ticket form.

A simple ticket system used consistently beats a full-featured help desk nobody opens. Ship the foundation, measure response times for a month, then add features based on data rather than guesses. GudangCode uses the same pattern for its member tickets.

Yudhi
Written by
Yudhi
Founder & Lead Developer, GudangCode

Yudhi is the founder of GudangCode and a Laravel developer who has built dozens of ready-to-use business information systems — from POS and HRIS to management apps. He writes guides and articles on GudangCode to help Indonesian developers run, understand, and deploy Laravel source code correctly.

LaravelPHPMySQLSistem Informasi Bisnis See all articles by Yudhi
Want the full source code & apps?

Sign up free to download ready-to-use business applications, information systems, and Laravel source code.

Sign Up Free & Download
Sistem Tiket Support Sistem Informasi
Share this article
Back to Blog
📚 Free Learning Hub

Learn Coding for Free at DhieCoderWeb

Explore Laravel, PHP, JavaScript tutorials, source code, web development guides, and practical programming tips.

DhieCoderWeb
100+
Tutorials
Free
Learning
SEO
Tips
Visit Dhiecoderweb.com →

Get Full Access Now!

Join our membership and unlock exclusive access to all premium features. Fast, easy, and ready to use instantly.

Join Membership Now