Every client wants a slightly different help desk. But underneath all those requests? The same familiar Laravel features, the same repetitive setup, and the same hours spent rebuilding things you’ve already built.
At some point, you have to ask yourself: Why am I building the same application for the fifth time?
If you work with Laravel, you probably know the situation.
A client needs a support portal. Nothing too complicated, they say.
Just authentication. Customer accounts. Support tickets. Priorities. Replies. File attachments. Email notifications. An admin dashboard. Agent permissions. Maybe internal notes so the support team can discuss a ticket without the customer seeing everything.
Oh, and could we have a dashboard with a few useful statistics?
Sure. No problem.
Then you open your editor, create the Laravel project, configure the database, set up authentication, build the ticket system, write the policies, create the dashboard, and start connecting everything together.
A few days later, you're looking at code you've written before. Not exactly the same code, of course. That would be too convenient. It's an older implementation from another project, with different assumptions, different dependencies, and just enough inconsistencies to make copying it more work than starting over.
I've been there.
So I decided to stop treating support portals as a new project every time and build a reusable foundation instead.
That's how NEXORA, my Laravel support portal starter kit, came together.
The goal wasn't to build another flashy SaaS dashboard with beautiful cards and meaningless charts. It was to package the repetitive, easy-to-overlook engineering work into something developers could actually use as a starting point.
And some of that work is more interesting than it sounds.
First, let's talk about the boring stuff
I like modern JavaScript frameworks as much as the next developer, but not every application needs a separate frontend, an API layer, and another build pipeline to maintain.
For this project, I kept things deliberately straightforward:
- Laravel 13 and PHP 8.3 for the application.
- MariaDB 10.11 for the database.
- Blade, Tailwind CSS, Alpine.js, and Vite for the interface.
- Laravel Breeze for authentication.
- DDEV for a reproducible development environment.
No separate SPA. No unnecessary API layer. No complicated frontend architecture just to display a list of tickets.
The objective was simple: give developers a foundation they can understand, customize, and extend without spending their first afternoon figuring out how everything fits together.
The environment is containerized, dependencies are locked in composer.lock, and the setup process is designed to be straightforward.
Because the last thing you need when starting a new project is to spend two hours debugging your local environment before you can even see the login page.
Three roles. One rule: permissions must actually mean something.
NEXORA has three main roles:
- Customer: Creates tickets, follows progress, and communicates with the support team.
- Support Agent: Manages tickets, responds to customers, and adds internal notes.
- Super Admin: Manages users, configuration, and the overall support operation.
That sounds pretty standard. The interesting part is how those permissions are enforced.
Hiding an admin button from a customer isn't security. Neither is hiding a URL in the navigation menu.
The application uses Laravel policies, middleware, and user-scoped queries to enforce access on the server.
For example:
public function view(User $user, Ticket $ticket): bool
{
return $user->isStaff()
|| $ticket->user_id === $user->id;
}
The rule is simple: staff can access tickets according to their permissions, while customers can only access their own.
And that rule isn't just sitting in a PHP file looking impressive. It's backed by automated tests.
If Alice tries to access Bob's ticket by manually changing the URL, the application rejects the request.
The same principle applies to replies, attachments, and other customer-specific resources.
I also included safeguards against a surprisingly unpleasant administrative problem: accidentally locking yourself out of your own application.
The system prevents the final active Super Admin from being demoted or deactivated, and prevents an administrator from deactivating their own account.
These aren't the features that make a product look impressive in a screenshot. They're the features you appreciate when something goes wrong.
The tiny problem that becomes a big problem: ticket numbers
Imagine a customer submits a support request.
They receive a reference number:
SUP-2026-000123
Looks simple enough.
Now imagine two customers submit tickets at almost exactly the same time.
If your application generates numbers using a naive MAX(id) + 1 approach, concurrent requests can interfere with one another. You might end up with duplicate numbers or unexpected failures.
It's an easy detail to overlook when you're building a prototype. It's less amusing when a real customer contacts support about ticket number SUP-2026-000123 and another customer has the same reference.
NEXORA uses a per-year sequence table, database transactions, and pessimistic row locking to coordinate number generation.
The simplified idea looks like this:
$sequence = DB::table('ticket_sequences')
->where('year', $year)
->lockForUpdate()
->first();
$next = $sequence->last_number + 1;
DB::table('ticket_sequences')
->where('year', $year)
->update(['last_number' => $next]);
return sprintf('SUP-%d-%06d', $year, $next);
The sequence update happens within the ticket creation transaction, and a unique database constraint provides an additional safeguard.
The important detail isn't the formatting of the reference number. It's the fact that generating it is treated as a concurrency problem rather than a string-formatting problem.
That's the difference between code that works in a quick demo and code designed for real application behavior.
Internal notes: the feature that must stay internal
Here's a familiar customer request:
“Can our support agents leave private notes on a ticket without the customer seeing them?”
Of course.
But consider what happens after you implement it.
The note doesn't appear on the customer-facing ticket page. Great.
Then a search query returns it. Or an email notification includes it. Or an export accidentally exposes it.
Suddenly, a feature that seemed simple has become a privacy problem.
For NEXORA, internal notes are handled as a data-access rule, not a visual preference.
Customer-facing ticket queries explicitly exclude internal replies:
'ticket' => $ticket->load([
'replies' => fn ($query) => $query
->where('is_internal', false)
->with(['user', 'attachments']),
]);
The same separation applies to search results, notifications, and activity timelines.
An internal note must not be sent to the customer through an email notification. Internal activity must not appear in the customer's timeline. Customer searches must not expose private note content.
I wrote tests specifically for these cases, including a test that uses a secret string in an internal note and checks that it doesn't leak through the customer-facing page, search, or email notifications.
This is the kind of feature where "it seems to work" isn't good enough.
The system needs to behave correctly even when someone accesses it in an unexpected way.
File uploads: because not every attachment should be public
Support tickets often contain screenshots, PDFs, documents, and other files.
Convenient, until you realize that a file uploaded by one customer shouldn't automatically become accessible to everyone with a URL.
NEXORA stores attachments on Laravel's private local disk, using UUID-based filenames. Original filenames are stored in the database rather than used directly as storage paths.
Downloads pass through a controller that:
- Authenticates the requester.
- Checks whether that user is authorized to access the attachment.
- Restricts internal attachments to staff.
- Streams the file as a download instead of rendering it inline in the browser.
Uploads are validated server-side against a configurable extension allowlist and file-size limit.
For example, PHP files are rejected by the upload validation rules, and unauthorized download attempts are covered by tests.
There's also an important limitation worth mentioning: the starter kit does not include malware scanning.
An extension allowlist is useful, but it isn't a replacement for antivirus scanning or a complete file-security strategy. Applications with stricter requirements should integrate an appropriate scanning service, such as ClamAV, and configure storage and access controls for their deployment environment.
I'd rather document that limitation clearly than pretend a validation rule solves every upload-security problem.
A Kanban board is easy. A correct workflow is harder.
The agent dashboard includes a Kanban-style board for moving tickets between statuses.
It's a familiar interaction: drag a card from Open to In Progress, and the board updates.
But the browser shouldn't be the authority on whether a transition is allowed.
A user can modify client-side code, send a direct HTTP request, or simply bypass the interface entirely.
That's why NEXORA validates ticket transitions on the server.
The lifecycle includes:
- Open
- In Progress
- Waiting for Customer
- Resolved
- Closed
Not every transition is valid. Moving a resolved ticket directly back into progress, for example, shouldn't be allowed simply because someone managed to drag a card there.
A dedicated TicketService::changeStatus() method validates the transition, updates the relevant timestamps, records an activity entry, and performs the operation inside a database transaction.
The Kanban board uses that same service.
If a transition is invalid, the server rejects it, the interface displays feedback, and the card returns to its previous position.
The principle is straightforward: the interface can make an action convenient, but the backend must decide whether it's permitted.
That keeps the workflow consistent whether a status change comes from a drag-and-drop interaction or another request to the application.
Dashboard statistics shouldn't be works of fiction
We've all seen dashboards that look impressive until someone asks where the numbers came from.
NEXORA calculates first-response and resolution averages from actual timestamps.
When there isn't enough data, it displays a message explaining that rather than inventing a statistic to fill an empty card.
It sounds obvious, but there's a meaningful difference between a dashboard that looks complete and one that tells the truth.
Seed data is useful for exploring the interface locally. It shouldn't become a substitute for real application metrics.
For a support team, these numbers can eventually influence staffing, workload planning, and service improvements. Their definitions should be clear, and their calculations should be grounded in actual events.
The test suite is part of the product
I didn't want NEXORA to be just a collection of controllers, models, and attractive dashboard screens.
The starter kit includes 78 automated tests covering 177 assertions.
The suite covers scenarios such as:
- Guests being blocked from protected areas.
- Disabled accounts losing access.
- Customers being prevented from viewing or modifying another customer's tickets.
- Unauthorized attachment downloads being rejected.
- Internal notes being excluded from customer-facing pages, search, and notifications.
- Role-escalation attempts being rejected.
- Invalid ticket transitions being blocked.
- Oversized or disallowed uploads being rejected.
- Administrative safeguards preventing the last active Super Admin from being removed.
The tests run against an in-memory SQLite database, keeping the test environment isolated from the application's normal data.
Before releasing a version, I run the suite with:
ddev exec php artisan test
Of course, a passing test suite doesn't prove that an application is free of vulnerabilities. It does provide something valuable: repeatable evidence that important behaviors work as intended.
It also makes the code easier to understand.
Want to know what happens when a customer tries to reopen a closed ticket? There's a test. Wondering whether internal notes trigger customer emails? There's a test for that, too.
Documentation explains how a system is supposed to work. Tests help demonstrate how it actually behaves.
What NEXORA doesn't include
I could describe this as a complete, production-ready support platform and fill the product page with ambitious promises.
That wouldn't be particularly useful.
NEXORA is a starter kit, not a finished enterprise help desk. It provides a foundation you can adapt to your own requirements.
It deliberately leaves some things outside its current scope:
- No built-in malware scanning for uploaded attachments.
- No rich-text message rendering; messages use escaped plain text.
- No two-factor authentication.
- No multilingual help-desk interface.
- No dedicated SLA-management or advanced reporting module.
You'll also need to handle the usual deployment responsibilities: HTTPS, backups, secret management, server patching, and environment-specific security configuration.
The idea isn't to solve every possible support workflow. It's to avoid making you rebuild the same core functionality before you can start solving the problems that make your project different.
Who is this for?
I built NEXORA with a few types of developers in mind.
Freelancers and agencies who repeatedly build customer portals for different clients and want a reusable foundation instead of another from-scratch implementation.
SaaS founders who need a support workflow without spending their first development sprint rebuilding authentication, ticket management, and role-based access.
Small teams that need an internal help desk for tracking requests, assigning responsibility, and managing conversations.
Laravel developers who prefer starting with a structured codebase and adapting it rather than assembling every familiar feature again.
There's also a static HTML/CSS/JavaScript demo included in the bundle, so you can explore the interface and present the visual direction to a client before setting up the Laravel application.
Getting started
The development workflow uses DDEV, so you don't need to install the application's PHP and database dependencies directly on your host machine.
The basic setup looks like this:
ddev start
ddev composer install
ddev exec php artisan key:generate
ddev exec php artisan migrate --seed
ddev npm install
ddev npm run build
Once the environment is running and the setup is complete, you can explore the portal, review the code, and start adapting it to your project.
For local development, the seeded demo accounts are:
- Admin:
admin@nexora.test - Support agent:
agent1@nexora.test - Customer:
alice@nexora.test
The demo password is password. These credentials are intended for local testing only and must never be used for a production deployment.
So, why did I package it?
Because I'd rather spend my time solving new problems than repeatedly rebuilding the same foundation.
Every support portal will eventually develop its own requirements. One client will need a different workflow. Another will need custom fields. Someone else will want integrations, SLA rules, or a completely different dashboard.
That's normal.
But authentication, ticket ownership, private notes, attachment permissions, status transitions, and basic administration shouldn't have to start from zero every time.
NEXORA is my attempt to turn that repetitive groundwork into something reusable, understandable, and easier to extend.
If you're building a support portal, an internal help desk, or a SaaS application with customer support, you can explore the demo and decide whether it's a useful starting point for your next project.
Explore the live demo: https://supportticketdemo.vercel.app/
Get NEXORA on Gumroad: [Insert your Gumroad product URL]
And if you do build something with it, I'd be interested to see what you create.
Tags: Laravel, PHP, Web Development, SaaS, Software Engineering
Top comments (0)