Skip to content
← Blog

Three interfaces in one Laravel project: site, panel and client portal

Burak Berk Şama · · 5 min read · Software Development
Three interfaces in one Laravel project: site, panel and client portal

When a company says "we need a website", it rarely means just one thing. On the Sardur Engineering project the need split into three parts: a public corporate site, a panel where the company manages its content and records, and a portal where customers reach their own maintenance history, post-cleaning photos and videos, and reports.

My first instinct was to build them as separate applications. In the end I kept all three in a single Laravel project; it is live today, and I believe that was the right call.

Why one project?

Three applications means three deployments, three dependency lists and an API to move data between them. That separation can make sense for large teams; on a one-person project it is mostly just cost.

The deciding factor was this: all three use the same data. A service description edited in the panel appears on the corporate site; a maintenance record created in the panel, along with its report, shows up for the customer in the portal. There is one domain model to share, and I did not want to write it twice.

Drawing the line in the routes

One project does not mean everything blends together. The separation is drawn clearly with route groups and middleware. The example below is a simplified illustration:

// Public corporate site
Route::get('/services', [ServiceController::class, 'index'])->name('services');

// Customer portal — signed-in customers only
Route::middleware(['auth', 'role:customer'])
    ->prefix('portal')
    ->name('portal.')
    ->group(function () {
        Route::get('/maintenance', [MaintenanceController::class, 'index'])->name('maintenance');
    });

I did not hand-build the admin panel; I used Filament. That single decision noticeably shortened the project: list and edit screens, file uploads, authorisation and search come ready-made. It let me spend the design effort on the side customers see — the corporate site and the portal.

Same data, different questions

The overlap between the portal and the panel was the most interesting part of the project. Both show the same maintenance records, but they answer different questions:

  • Panel: "Which customer, which facility, and when was it serviced?"
  • Portal: "When was my facility last serviced, what did it look like, and where is the report?"

I solved this with authorisation and a query scope, not two separate models. A customer user's queries are automatically limited to their own records by a scope defined on the model (again, simplified):

public function scopeVisibleTo(Builder $query, User $user): Builder
{
    return $user->isStaff()
        ? $query
        : $query->where('customer_id', $user->customer_id);
}

Putting the scope on the model makes "forgetting to filter somewhere" impossible by construction.

Reports: file uploads are a security decision

Maintenance reports are uploaded to the panel as PDFs and downloaded by the customer from the portal. That calls for treating uploads not as a simple form field but as an access decision:

  • Files are kept not in a public folder but in storage that cannot be reached by direct URL.
  • Every download request is checked on the server: does this report belong to the requesting customer's own record?
  • Size and type limits are enforced at upload time.

Storage, limits and access permission are not three separate details; they are one decision to be designed together.

One-click updates

This was added later in the project, and it is the feature that made the biggest difference on the customer side. Once a cleaning job is done, the owner uploads the photos and videos taken on site to the relevant record in the panel, then sends the customer an update email with a single button. The customer can review the completed work, with visual evidence, in the portal.

The decision I value most here is that sending is not triggered automatically: the email goes out only when the owner has uploaded the content and decides it is ready. A half-finished or incomplete record never reaches the customer, and the timing stays entirely in the hands of the person who did the work. On the technical side, the email relies on the same authorisation rules as the portal: a notification only ever goes to the customer who owns the record.

Three principles

Principles that work for projects like this, and that I will apply from day one next time:

  1. Authorisation does not live in the view. Hiding a link in the menu is not access control; anyone who knows the address can reach the page. The check belongs in the route and the query.
  2. Information that can change does not live in code. Company name, contact details or opening hours should sit in the database and be editable from the panel, so the company is not dependent on a developer for small changes.
  3. File access is designed on day one. If there are customer-specific documents such as reports, storage and download permissions should be planned at the start of the project.

Takeaway

Three interfaces in one project is the right scale for a team of one. As long as you draw the boundary with route groups, middleware and query scopes rather than folder structure, the interfaces stay untangled and the shared data model becomes the project's greatest strength.

Laravel PHP Web Full-stack